apex-ui-kit 2.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Licence.md +21 -0
- package/ReadMe.md +75 -0
- package/dist/ApexUI-Kit/templates/Avatar/Avatar.jsx +81 -0
- package/dist/ApexUI-Kit/templates/BeamBar/BeamBar.jsx +101 -0
- package/dist/ApexUI-Kit/templates/Cards/BasicCard.jsx +60 -0
- package/dist/ApexUI-Kit/templates/Carousel/Carousel.jsx +95 -0
- package/dist/ApexUI-Kit/templates/Carousel/DragCarousel.jsx +105 -0
- package/dist/ApexUI-Kit/templates/Carousel/ParallaxCarousel.jsx +161 -0
- package/dist/ApexUI-Kit/templates/DragCarousel/DragCarousel.jsx +105 -0
- package/dist/ApexUI-Kit/templates/EdgeGlowForm/EdgeGlowFrom.jsx +297 -0
- package/dist/ApexUI-Kit/templates/FlipProfile/FlipProfile.jsx +125 -0
- package/dist/ApexUI-Kit/templates/GlareCard/GlareCard.jsx +146 -0
- package/dist/ApexUI-Kit/templates/GlassCardStack/GlassCardStack.jsx +111 -0
- package/dist/ApexUI-Kit/templates/HoverText/HoverText.jsx +371 -0
- package/dist/ApexUI-Kit/templates/HyperCard/HyperCard.jsx +183 -0
- package/dist/ApexUI-Kit/templates/KineticThreadsBackground/KineticThreadBackground.jsx +249 -0
- package/dist/ApexUI-Kit/templates/LensFlareBackground/LensFlareBackground.jsx +268 -0
- package/dist/ApexUI-Kit/templates/LuminousParticleOcean/LuminousParticleOcean.jsx +155 -0
- package/dist/ApexUI-Kit/templates/NavDropDown/NavDropDown.jsx +114 -0
- package/dist/ApexUI-Kit/templates/NavMenu/NavMenu.jsx +153 -0
- package/dist/ApexUI-Kit/templates/OrbsBackground/FloatingOrbs.jsx +96 -0
- package/dist/ApexUI-Kit/templates/ParallaxCard/ParallaxCard.jsx +127 -0
- package/dist/ApexUI-Kit/templates/ParallaxCarousel/ParallaxCarousel.jsx +161 -0
- package/dist/ApexUI-Kit/templates/ParallaxProfile/ParallaxProfile.jsx +107 -0
- package/dist/ApexUI-Kit/templates/PointerFollower/PointerFollower.jsx +126 -0
- package/dist/ApexUI-Kit/templates/RainBackground/RainBackground.jsx +109 -0
- package/dist/ApexUI-Kit/templates/RippleBackground/RippleBackground.jsx +160 -0
- package/dist/ApexUI-Kit/templates/ScrollMarquee/ScrollMarquee.jsx +174 -0
- package/dist/ApexUI-Kit/templates/SmartGridCard/SmartGridCard.jsx +241 -0
- package/dist/ApexUI-Kit/templates/SmokeBackground/SmokeBackground.jsx +216 -0
- package/dist/ApexUI-Kit/templates/Testimonials/Testimonials.jsx +117 -0
- package/dist/ApexUI-Kit/templates/ThemeToggle/ThemeAnimation.js +252 -0
- package/dist/ApexUI-Kit/templates/ThemeToggle/ThemeConfig.js +8 -0
- package/dist/ApexUI-Kit/templates/ThemeToggle/ThemeToggle.css +560 -0
- package/dist/ApexUI-Kit/templates/ThemeToggle/ThemeToggle.jsx +118 -0
- package/dist/ApexUI-Kit/templates/ToolTip/ToolTip.jsx +85 -0
- package/dist/ApexUI-Kit/templates/WaterDropReveal/WaterDropReveal.jsx +151 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.js +145 -0
- package/package.json +56 -0
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/* eslint-disable no-unused-vars */
|
|
2
|
+
import React, { useState } from "react";
|
|
3
|
+
import { motion, AnimatePresence } from "framer-motion";
|
|
4
|
+
import { BsTwitterX } from "react-icons/bs";
|
|
5
|
+
// --- ICONS (unchanged) ---
|
|
6
|
+
const GitHubIcon = () => (
|
|
7
|
+
<svg className="w-5 h-5" fill="none" stroke="currentColor" strokeWidth="1.5" viewBox="0 0 24 24">
|
|
8
|
+
<path d="M9 19c-5 1.5-5-2.5-7-3m14 6v-3.87a3.37 3.37 0 0 0-.94-2.61c3.14-.35 6.44-1.54 6.44-7A5.44 5.44 0 0 0 20 4.77A5.07 5.07 0 0 0 19.91 1S18.73.65 16 2.48a13.38 13.38 0 0 0-7 0C6.27.65 5.09 1 5.09 1A5.07 5.07 0 0 0 5 4.77a5.44 5.44 0 0 0-1.54 2.78c0 5.42 3.3 6.61 6.44 7A3.37 3.37 0 0 0 9 18.13V22"></path>
|
|
9
|
+
</svg>
|
|
10
|
+
);
|
|
11
|
+
const LinkedInIcon = () => (
|
|
12
|
+
<svg className="w-5 h-5" fill="none" stroke="currentColor" strokeWidth="1.5" viewBox="0 0 24 24">
|
|
13
|
+
<path d="M16 8a6 6 0 0 1 6 6v7h-4v-7a2 2 0 0 0-2-2 2 2 0 0 0-2 2v7h-4v-7a6 6 0 0 1 6-6z"></path>
|
|
14
|
+
<rect x="2" y="9" width="4" height="12"></rect>
|
|
15
|
+
<circle cx="4" cy="4" r="2"></circle>
|
|
16
|
+
</svg>
|
|
17
|
+
);
|
|
18
|
+
const TwitterIcon = () => (
|
|
19
|
+
<BsTwitterX size={17} />
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
const FlipProfile = ({
|
|
24
|
+
name = "Iron Man",
|
|
25
|
+
role = "AI Engineer",
|
|
26
|
+
dob = "5th March, 1999",
|
|
27
|
+
avatar = "/assets/ironman.png",
|
|
28
|
+
github = "#",
|
|
29
|
+
linkedin = "#",
|
|
30
|
+
twitter = "#",
|
|
31
|
+
resumeLink = "/Iron-Man-Resume.pdf",
|
|
32
|
+
hireLink = "#",
|
|
33
|
+
bgColor = "#0F172B"
|
|
34
|
+
}) => {
|
|
35
|
+
const [isFlipped, setIsFlipped] = useState(false);
|
|
36
|
+
|
|
37
|
+
const cardVariants = {
|
|
38
|
+
flipped: { rotateY: 180 },
|
|
39
|
+
initial: { rotateY: 0 }
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
// Staggered animation for the back content
|
|
43
|
+
const backContentVariants = {
|
|
44
|
+
hidden: { opacity: 0 },
|
|
45
|
+
visible: {
|
|
46
|
+
opacity: 1,
|
|
47
|
+
transition: { staggerChildren: 0.1, delayChildren: 0.3 }
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const itemVariants = {
|
|
52
|
+
hidden: { opacity: 0, y: 20 },
|
|
53
|
+
visible: { opacity: 1, y: 0 }
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
return (
|
|
57
|
+
<div className="w-full flex items-center justify-center p-4 min-h-[450px]" style={{ perspective: "1200px" }}>
|
|
58
|
+
<motion.div
|
|
59
|
+
className="relative w-full max-w-sm h-[420px] cursor-pointer"
|
|
60
|
+
style={{ transformStyle: "preserve-3d" }}
|
|
61
|
+
variants={cardVariants}
|
|
62
|
+
animate={isFlipped ? "flipped" : "initial"}
|
|
63
|
+
transition={{ duration: 0.7, ease: "easeInOut" }}
|
|
64
|
+
onClick={() => setIsFlipped(!isFlipped)}
|
|
65
|
+
>
|
|
66
|
+
{/* --- FRONT OF THE CARD --- */}
|
|
67
|
+
<motion.div
|
|
68
|
+
className="absolute w-full h-full rounded-3xl overflow-hidden shadow-2xl border border-[#B0DC3C] backdrop-blur-md flex flex-col items-center justify-center p-6 text-center text-white"
|
|
69
|
+
style={{
|
|
70
|
+
background: bgColor,
|
|
71
|
+
backfaceVisibility: "hidden"
|
|
72
|
+
}}
|
|
73
|
+
>
|
|
74
|
+
<div className="absolute inset-0 w-full h-full border-2 border-[#B0DC3C] rounded-3xl animate-pulse pointer-events-none opacity-30"></div>
|
|
75
|
+
<img src={avatar} alt="Avatar" className="w-32 h-32 rounded-full border-4 border-[#B0DC3C] object-cover shadow-lg shadow-fuchsia-900/50 mb-4" />
|
|
76
|
+
<h2 className="text-3xl font-bold bg-gradient-to-r from-gray-500 to-white bg-clip-text text-transparent">{name}</h2>
|
|
77
|
+
<p className="text-lg font-medium text-green-200/90">{role}</p>
|
|
78
|
+
<p className="mt-4 text-xs text-gray-100">Click to flip</p>
|
|
79
|
+
</motion.div>
|
|
80
|
+
|
|
81
|
+
{/* --- BACK OF THE CARD --- */}
|
|
82
|
+
<motion.div
|
|
83
|
+
className="absolute w-full h-full rounded-3xl overflow-hidden shadow-2xl border border-cyan-500/40 backdrop-blur-md flex flex-col items-center justify-center p-6 text-center text-white"
|
|
84
|
+
style={{
|
|
85
|
+
background: bgColor,
|
|
86
|
+
backfaceVisibility: "hidden",
|
|
87
|
+
transform: "rotateY(180deg)"
|
|
88
|
+
}}
|
|
89
|
+
>
|
|
90
|
+
<AnimatePresence>
|
|
91
|
+
{isFlipped && (
|
|
92
|
+
<motion.div
|
|
93
|
+
className="flex flex-col items-center justify-center w-full h-full"
|
|
94
|
+
variants={backContentVariants}
|
|
95
|
+
initial="hidden"
|
|
96
|
+
animate="visible"
|
|
97
|
+
exit="hidden"
|
|
98
|
+
>
|
|
99
|
+
<motion.p variants={itemVariants} className="text-sm text-cyan-300">DOB: {dob}</motion.p>
|
|
100
|
+
<motion.div variants={itemVariants} className="w-1/2 h-px bg-cyan-400/30 my-4" />
|
|
101
|
+
|
|
102
|
+
<motion.div variants={itemVariants} className="flex gap-5 items-center justify-center text-cyan-300">
|
|
103
|
+
<a href={github} target="_blank" rel="noopener noreferrer" className="hover:text-white transition-colors"><GitHubIcon /></a>
|
|
104
|
+
<a href={linkedin} target="_blank" rel="noopener noreferrer" className="hover:text-white transition-colors"><LinkedInIcon /></a>
|
|
105
|
+
<a href={twitter} target="_blank" rel="noopener noreferrer" className="hover:text-white transition-colors"><TwitterIcon /></a>
|
|
106
|
+
</motion.div>
|
|
107
|
+
|
|
108
|
+
<motion.div variants={itemVariants} className="mt-6 flex flex-col gap-3 w-full items-center">
|
|
109
|
+
<motion.a whileTap={{ scale: 0.95 }} href={hireLink} className="w-full max-w-xs text-center px-6 py-2.5 bg-cyan-600/80 hover:bg-cyan-500 text-white rounded-full font-semibold shadow-lg shadow-cyan-900/40 transition-all">
|
|
110
|
+
Hire Me
|
|
111
|
+
</motion.a>
|
|
112
|
+
<motion.a whileTap={{ scale: 0.95 }} href={resumeLink} download className="w-full max-w-xs text-center px-6 py-2.5 bg-white/10 hover:bg-white/20 text-white rounded-full font-semibold backdrop-blur-md transition-all">
|
|
113
|
+
Download CV
|
|
114
|
+
</motion.a>
|
|
115
|
+
</motion.div>
|
|
116
|
+
</motion.div>
|
|
117
|
+
)}
|
|
118
|
+
</AnimatePresence>
|
|
119
|
+
</motion.div>
|
|
120
|
+
</motion.div>
|
|
121
|
+
</div>
|
|
122
|
+
);
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
export default FlipProfile;
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/* eslint-disable no-unused-vars */
|
|
2
|
+
|
|
3
|
+
function cn(...classes) {
|
|
4
|
+
return classes.filter(Boolean).join(" ");
|
|
5
|
+
}
|
|
6
|
+
import React, { useRef } from "react";
|
|
7
|
+
import { motion, useMotionValue, useSpring, useMotionTemplate } from "framer-motion";
|
|
8
|
+
import bgHexa from '/assets/bg-hexa.jpg'
|
|
9
|
+
export default function GlareCardDemo() {
|
|
10
|
+
return (
|
|
11
|
+
<GlareCard className="flex flex-col items-center justify-center bg-gradient-to-br from-gray-900 via-slate-800 to-[#181825] p-8 border border-slate-800 shadow-2xl relative overflow-hidden group group-hover:scale-100 transition-transform duration-300 ease-in-out">
|
|
12
|
+
{/* Background Image */}
|
|
13
|
+
<img
|
|
14
|
+
src={bgHexa}
|
|
15
|
+
alt="bg"
|
|
16
|
+
className="absolute inset-0 w-full h-full object-cover opacity-30 pointer-events-none select-none z-0"
|
|
17
|
+
draggable="false"
|
|
18
|
+
/>
|
|
19
|
+
<div className="flex items-center justify-center w-20 h-20 rounded-md bg-gradient-to-br from-[#23233a] to-[#181825] border-2 border-[#a855f7]/60 shadow-xl mb-4 z-10 group">
|
|
20
|
+
<span className="text-5xl font-extrabold text-[#a855f7] drop-shadow-[0_2px_12px_#a855f7aa]">A</span>
|
|
21
|
+
</div>
|
|
22
|
+
<p className="text-slate-200 font-extrabold text-2xl mt-2 tracking-wide z-10">ApexUI</p>
|
|
23
|
+
</GlareCard>
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function GlareCard({
|
|
28
|
+
children,
|
|
29
|
+
className = "",
|
|
30
|
+
backgroundImage = bgHexa,
|
|
31
|
+
foilSvg = `url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100' width='26' height='26' fill='white'%3E%3Cpath d='M10 90 L40 10 L60 10 L90 90 L70 90 L62 70 H38 L30 90 Z M45 50 H55 L50 30 Z' fill='white'/%3E%3C/svg%3E")`
|
|
32
|
+
}) {
|
|
33
|
+
const refElement = useRef(null);
|
|
34
|
+
// Framer Motion values
|
|
35
|
+
const glareX = useMotionValue(50);
|
|
36
|
+
const glareY = useMotionValue(50);
|
|
37
|
+
const bgX = useMotionValue(50);
|
|
38
|
+
const bgY = useMotionValue(50);
|
|
39
|
+
const rX = useMotionValue(0);
|
|
40
|
+
const rY = useMotionValue(0);
|
|
41
|
+
// Springs for smoothness
|
|
42
|
+
const springConfig = { stiffness: 120, damping: 18 };
|
|
43
|
+
const sGlareX = useSpring(glareX, springConfig);
|
|
44
|
+
const sGlareY = useSpring(glareY, springConfig);
|
|
45
|
+
const sBgX = useSpring(bgX, springConfig);
|
|
46
|
+
const sBgY = useSpring(bgY, springConfig);
|
|
47
|
+
const sRX = useSpring(rX, springConfig);
|
|
48
|
+
const sRY = useSpring(rY, springConfig);
|
|
49
|
+
|
|
50
|
+
// CSS variable template
|
|
51
|
+
const style = {
|
|
52
|
+
"--m-x": useMotionTemplate`${sGlareX}%`,
|
|
53
|
+
"--m-y": useMotionTemplate`${sGlareY}%`,
|
|
54
|
+
"--r-x": useMotionTemplate`${sRX}deg`,
|
|
55
|
+
"--r-y": useMotionTemplate`${sRY}deg`,
|
|
56
|
+
"--bg-x": useMotionTemplate`${sBgX}%`,
|
|
57
|
+
"--bg-y": useMotionTemplate`${sBgY}%`,
|
|
58
|
+
"--duration": "300ms",
|
|
59
|
+
"--foil-size": "100%",
|
|
60
|
+
"--opacity": "0",
|
|
61
|
+
"--radius": "10px",
|
|
62
|
+
"--easing": "ease",
|
|
63
|
+
"--transition": "var(--duration) var(--easing)"
|
|
64
|
+
};
|
|
65
|
+
const backgroundStyle = {
|
|
66
|
+
"--step": "5%",
|
|
67
|
+
"--foil-svg": foilSvg,
|
|
68
|
+
"--pattern": "var(--foil-svg) center/100% no-repeat",
|
|
69
|
+
"--rainbow":
|
|
70
|
+
"repeating-linear-gradient( 0deg,rgb(255,119,115) calc(var(--step) * 1),rgba(255,237,95,1) calc(var(--step) * 2),rgba(168,255,95,1) calc(var(--step) * 3),rgba(131,255,247,1) calc(var(--step) * 4),rgba(120,148,255,1) calc(var(--step) * 5),rgb(216,117,255) calc(var(--step) * 6),rgb(255,119,115) calc(var(--step) * 7) ) 0% var(--bg-y)/200% 700% no-repeat",
|
|
71
|
+
"--diagonal":
|
|
72
|
+
"repeating-linear-gradient( 128deg,#0e152e 0%,hsl(180,10%,60%) 3.8%,hsl(180,10%,60%) 4.5%,hsl(180,10%,60%) 5.2%,#0e152e 10%,#0e152e 12% ) var(--bg-x) var(--bg-y)/300% no-repeat",
|
|
73
|
+
"--shade":
|
|
74
|
+
"radial-gradient( farthest-corner circle at var(--m-x) var(--m-y),rgba(255,255,255,0.1) 12%,rgba(255,255,255,0.15) 20%,rgba(255,255,255,0.25) 120% ) var(--bg-x) var(--bg-y)/300% no-repeat",
|
|
75
|
+
backgroundBlendMode: "hue, hue, hue, overlay",
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
return (
|
|
79
|
+
<motion.div
|
|
80
|
+
style={style}
|
|
81
|
+
className="relative isolate [contain:layout_style] [perspective:600px] transition-transform duration-[var(--duration)] ease-[var(--easing)] delay-[var(--delay)] will-change-transform w-[320px] [aspect-ratio:17/21]"
|
|
82
|
+
ref={refElement}
|
|
83
|
+
onPointerMove={(event) => {
|
|
84
|
+
const rotateFactor = 0.4;
|
|
85
|
+
const rect = event.currentTarget.getBoundingClientRect();
|
|
86
|
+
const position = {
|
|
87
|
+
x: event.clientX - rect.left,
|
|
88
|
+
y: event.clientY - rect.top,
|
|
89
|
+
};
|
|
90
|
+
const percentage = {
|
|
91
|
+
x: (100 / rect.width) * position.x,
|
|
92
|
+
y: (100 / rect.height) * position.y,
|
|
93
|
+
};
|
|
94
|
+
const delta = {
|
|
95
|
+
x: percentage.x - 50,
|
|
96
|
+
y: percentage.y - 50,
|
|
97
|
+
};
|
|
98
|
+
bgX.set(50 + percentage.x / 4 - 12.5);
|
|
99
|
+
bgY.set(50 + percentage.y / 3 - 16.67);
|
|
100
|
+
rX.set(-(delta.x / 3.5) * rotateFactor);
|
|
101
|
+
rY.set(delta.y / 2 * rotateFactor);
|
|
102
|
+
glareX.set(percentage.x);
|
|
103
|
+
glareY.set(percentage.y);
|
|
104
|
+
}}
|
|
105
|
+
onPointerEnter={() => {
|
|
106
|
+
if (refElement.current) {
|
|
107
|
+
setTimeout(() => {
|
|
108
|
+
refElement.current?.style.setProperty("--duration", "0s");
|
|
109
|
+
}, 300);
|
|
110
|
+
}
|
|
111
|
+
}}
|
|
112
|
+
onPointerLeave={() => {
|
|
113
|
+
if (refElement.current) {
|
|
114
|
+
refElement.current.style.removeProperty("--duration");
|
|
115
|
+
rX.set(0);
|
|
116
|
+
rY.set(0);
|
|
117
|
+
}
|
|
118
|
+
}}
|
|
119
|
+
>
|
|
120
|
+
<div
|
|
121
|
+
className="h-full grid will-change-transform origin-center transition-transform duration-[var(--duration)] ease-[var(--easing)] delay-[var(--delay)] [transform:rotateY(var(--r-x))_rotateX(var(--r-y))] rounded-[var(--radius)] border border-slate-800 hover:[--opacity:0.6] hover:[--duration:200ms] hover:[--easing:linear] hover:filter-none overflow-hidden"
|
|
122
|
+
>
|
|
123
|
+
<div
|
|
124
|
+
className="w-full h-full grid [grid-area:1/1] mix-blend-soft-light [clip-path:inset(0_0_0_0_round_var(--radius))]">
|
|
125
|
+
<div className={cn("h-full w-full bg-slate-950", className)}>
|
|
126
|
+
{/* Background Image Layer (if provided) */}
|
|
127
|
+
{backgroundImage && (
|
|
128
|
+
<img
|
|
129
|
+
src={backgroundImage}
|
|
130
|
+
alt="bg"
|
|
131
|
+
className="absolute inset-0 w-full h-full object-cover opacity-30 pointer-events-none select-none z-0"
|
|
132
|
+
draggable="false"
|
|
133
|
+
/>
|
|
134
|
+
)}
|
|
135
|
+
{children}
|
|
136
|
+
</div>
|
|
137
|
+
</div>
|
|
138
|
+
<div
|
|
139
|
+
className="w-full h-full grid [grid-area:1/1] mix-blend-soft-light [clip-path:inset(0_0_1px_0_round_var(--radius))] opacity-[var(--opacity)] transition-opacity transition-background duration-[var(--duration)] ease-[var(--easing)] delay-[var(--delay)] will-change-background [background:radial-gradient(farthest-corner_circle_at_var(--m-x)_var(--m-y),_rgba(255,255,255,0.8)_10%,_rgba(255,255,255,0.65)_20%,_rgba(255,255,255,0)_90%)]" />
|
|
140
|
+
<div
|
|
141
|
+
className="w-full h-full grid [grid-area:1/1] mix-blend-color-dodge opacity-[var(--opacity)] will-change-background transition-opacity [clip-path:inset(0_0_1px_0_round_var(--radius))] [background-blend-mode:hue_hue_hue_overlay] [background:var(--pattern),_var(--rainbow),_var(--diagonal),_var(--shade)] relative after:content-[''] after:grid-area-[inherit] after:bg-repeat-[inherit] after:bg-attachment-[inherit] after:bg-origin-[inherit] after:bg-clip-[inherit] after:bg-[inherit] after:mix-blend-exclusion after:[background-size:var(--foil-size),_200%_400%,_800%,_200%] after:[background-position:center,_0%_var(--bg-y),_calc(var(--bg-x)*_-1)_calc(var(--bg-y)*_-1),_var(--bg-x)_var(--bg-y)] after:[background-blend-mode:soft-light,_hue,_hard-light]"
|
|
142
|
+
style={{ ...backgroundStyle }} />
|
|
143
|
+
</div>
|
|
144
|
+
</motion.div>
|
|
145
|
+
);
|
|
146
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/* eslint-disable no-unused-vars */
|
|
2
|
+
import React, { useState, useEffect, useRef } from "react";
|
|
3
|
+
import { motion, AnimatePresence } from "framer-motion";
|
|
4
|
+
import { FaExternalLinkAlt, FaGithub, FaRegImage } from "react-icons/fa";
|
|
5
|
+
const CARD_STACK_OFFSET = 32;
|
|
6
|
+
const CARD_SCALE_STEP = 0.08;
|
|
7
|
+
const AUTO_CYCLE_INTERVAL = 5000;
|
|
8
|
+
export default function GlassCardStack({
|
|
9
|
+
cards = [],
|
|
10
|
+
autoCycle = true,
|
|
11
|
+
cycleInterval = AUTO_CYCLE_INTERVAL,
|
|
12
|
+
}) {
|
|
13
|
+
const [active, setActive] = useState(0);
|
|
14
|
+
const [paused, setPaused] = useState(false);
|
|
15
|
+
const timeoutRef = useRef();
|
|
16
|
+
|
|
17
|
+
useEffect(() => {
|
|
18
|
+
if (!autoCycle || paused) return;
|
|
19
|
+
timeoutRef.current = setTimeout(() => {
|
|
20
|
+
setActive((prev) => (prev + 1) % cards.length);
|
|
21
|
+
}, cycleInterval);
|
|
22
|
+
return () => clearTimeout(timeoutRef.current);
|
|
23
|
+
}, [active, paused, autoCycle, cycleInterval, cards.length]);
|
|
24
|
+
|
|
25
|
+
// For cycling animation
|
|
26
|
+
const getCardIndex = (i) => (active + i) % cards.length;
|
|
27
|
+
|
|
28
|
+
return (
|
|
29
|
+
<div
|
|
30
|
+
className="relative flex justify-center items-center h-[340px] md:h-[400px] w-full max-w-lg mx-auto select-none"
|
|
31
|
+
onMouseEnter={() => setPaused(true)}
|
|
32
|
+
onMouseLeave={() => setPaused(false)}
|
|
33
|
+
>
|
|
34
|
+
{cards.map((_, i) => {
|
|
35
|
+
// Stack order: 0 is top, 1 is next, etc.
|
|
36
|
+
const idx = getCardIndex(i);
|
|
37
|
+
const isTop = i === 0;
|
|
38
|
+
const z = cards.length - i;
|
|
39
|
+
const offset = i * CARD_STACK_OFFSET;
|
|
40
|
+
const scale = 1 - i * CARD_SCALE_STEP;
|
|
41
|
+
const card = cards[idx];
|
|
42
|
+
return (
|
|
43
|
+
<AnimatePresence key={idx}>
|
|
44
|
+
<motion.div
|
|
45
|
+
key={idx}
|
|
46
|
+
initial={{
|
|
47
|
+
y: 60,
|
|
48
|
+
opacity: 0,
|
|
49
|
+
scale: scale * 0.95,
|
|
50
|
+
}}
|
|
51
|
+
animate={{
|
|
52
|
+
y: -offset,
|
|
53
|
+
opacity: 1,
|
|
54
|
+
scale,
|
|
55
|
+
zIndex: z,
|
|
56
|
+
filter: isTop ? "none" : "blur(2px)",
|
|
57
|
+
}}
|
|
58
|
+
exit={{
|
|
59
|
+
y: -100,
|
|
60
|
+
opacity: 0,
|
|
61
|
+
scale: scale * 0.95,
|
|
62
|
+
}}
|
|
63
|
+
transition={{ type: "spring", stiffness: 300, damping: 30 }}
|
|
64
|
+
className={`absolute left-0 right-0 mx-auto w-11/12 md:w-96 h-48 md:h-60 rounded-3xl shadow-xl bg-gradient-to-br ${card.color} backdrop-blur-lg border border-white/20 flex flex-row items-center px-3 md:px-6 py-4 cursor-pointer select-none ${isTop ? "" : "pointer-events-none"}`}
|
|
65
|
+
style={{ zIndex: z }}
|
|
66
|
+
>
|
|
67
|
+
{/* Left: Responsive Image */}
|
|
68
|
+
<div className="flex-shrink-0 flex items-center justify-center w-16 h-16 md:w-24 md:h-24 lg:w-28 lg:h-28 rounded-xl overflow-hidden bg-white/10 border border-white/20 mr-3 md:mr-6">
|
|
69
|
+
{card.image ? (
|
|
70
|
+
<img
|
|
71
|
+
src={card.image}
|
|
72
|
+
alt={card.title}
|
|
73
|
+
className="object-cover w-full h-full"
|
|
74
|
+
/>
|
|
75
|
+
) : (
|
|
76
|
+
<FaRegImage className="text-3xl text-white/40" />
|
|
77
|
+
)}
|
|
78
|
+
</div>
|
|
79
|
+
{/* Right: Info */}
|
|
80
|
+
<div className="flex-1 flex flex-col justify-center gap-1 md:gap-2">
|
|
81
|
+
<div className="flex items-center gap-2">
|
|
82
|
+
<h2 className="text-base md:text-xl font-bold text-white drop-shadow">
|
|
83
|
+
{card.title}
|
|
84
|
+
</h2>
|
|
85
|
+
{card.github && (
|
|
86
|
+
<a href={card.github} target="_blank" rel="noopener noreferrer" className="text-white/70 hover:text-white ml-1">
|
|
87
|
+
<FaGithub className="inline-block align-middle text-lg md:text-xl" />
|
|
88
|
+
</a>
|
|
89
|
+
)}
|
|
90
|
+
</div>
|
|
91
|
+
<p className="text-xs md:text-sm text-white/60 mb-1">{card.subtitle}</p>
|
|
92
|
+
<p className="text-xs md:text-base text-white/80 mb-1 line-clamp-2">{card.desc}</p>
|
|
93
|
+
{card.link && (
|
|
94
|
+
<a
|
|
95
|
+
href={card.link}
|
|
96
|
+
target="_blank"
|
|
97
|
+
rel="noopener noreferrer"
|
|
98
|
+
className="inline-flex items-center gap-1 text-xs md:text-sm px-2 py-1 rounded bg-blue-500/80 hover:bg-blue-600/90 text-white font-semibold shadow transition w-fit mt-1"
|
|
99
|
+
>
|
|
100
|
+
<FaExternalLinkAlt className="inline-block mr-1 text-xs md:text-sm" />
|
|
101
|
+
Visit
|
|
102
|
+
</a>
|
|
103
|
+
)}
|
|
104
|
+
</div>
|
|
105
|
+
</motion.div>
|
|
106
|
+
</AnimatePresence>
|
|
107
|
+
);
|
|
108
|
+
})}
|
|
109
|
+
</div>
|
|
110
|
+
);
|
|
111
|
+
}
|