apex-ui-kit 2.0.6 → 2.1.0

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.
Files changed (35) hide show
  1. package/dist/ApexUI-Kit/templates/AccordionMarquee/AccordionMarquee.jsx +74 -53
  2. package/dist/ApexUI-Kit/templates/BeamBar/BeamBar.jsx +5 -3
  3. package/dist/ApexUI-Kit/templates/BluePrintBackground/BluePrintBackground.jsx +14 -2
  4. package/dist/ApexUI-Kit/templates/CardStack/CardStack.jsx +129 -0
  5. package/dist/ApexUI-Kit/templates/Cards/BasicCard.jsx +3 -3
  6. package/dist/ApexUI-Kit/templates/Carousel/ParallaxCarousel.jsx +0 -161
  7. package/dist/ApexUI-Kit/templates/DragCarousel/DragCarousel.jsx +39 -7
  8. package/dist/ApexUI-Kit/templates/EdgeGlowForm/EdgeGlowFrom.jsx +116 -98
  9. package/dist/ApexUI-Kit/templates/FlipProfile/FlipProfile.jsx +69 -37
  10. package/dist/ApexUI-Kit/templates/GlareCard/GlareCard.jsx +2 -2
  11. package/dist/ApexUI-Kit/templates/HoverText/HoverText.jsx +14 -13
  12. package/dist/ApexUI-Kit/templates/HyperCard/HyperCard.jsx +59 -42
  13. package/dist/ApexUI-Kit/templates/KineticThreadsBackground/KineticThreadBackground.jsx +17 -2
  14. package/dist/ApexUI-Kit/templates/LensFlareBackground/LensFlareBackground.jsx +18 -3
  15. package/dist/ApexUI-Kit/templates/LuminousParticleOcean/LuminousParticleOcean.jsx +15 -1
  16. package/dist/ApexUI-Kit/templates/MaskView/MaskView.jsx +345 -0
  17. package/dist/ApexUI-Kit/templates/MeteorCard/MeteorCard.jsx +34 -45
  18. package/dist/ApexUI-Kit/templates/NavDropDown/NavDropDown.jsx +272 -101
  19. package/dist/ApexUI-Kit/templates/NavMenu/NavMenu.jsx +8 -2
  20. package/dist/ApexUI-Kit/templates/ParallaxCard/ParallaxCard.jsx +13 -11
  21. package/dist/ApexUI-Kit/templates/ParallaxCarousel/ParallaxCarousel.jsx +60 -67
  22. package/dist/ApexUI-Kit/templates/ParallaxProfile/ParallaxProfile.jsx +69 -55
  23. package/dist/ApexUI-Kit/templates/PointerFollower/PointerFollower.jsx +34 -14
  24. package/dist/ApexUI-Kit/templates/ScrollMarquee/CardMarquee.jsx +0 -0
  25. package/dist/ApexUI-Kit/templates/SmartGridCard/SmartGridCard.jsx +4 -3
  26. package/dist/ApexUI-Kit/templates/SmokeBackground/SmokeBackground.jsx +35 -16
  27. package/dist/ApexUI-Kit/templates/Testimonials/Testimonials.jsx +11 -8
  28. package/dist/ApexUI-Kit/templates/ThemeToggle/ThemeAnimation.js +40 -69
  29. package/dist/ApexUI-Kit/templates/ThemeToggle/ThemeToggle.css +5 -1
  30. package/dist/ApexUI-Kit/templates/ThemeToggle/ThemeToggle.jsx +25 -7
  31. package/dist/ApexUI-Kit/templates/ToolTip/ToolTip.jsx +71 -52
  32. package/dist/cli/index.js +25 -14
  33. package/package.json +2 -2
  34. package/dist/ApexUI-Kit/templates/GlassCardStack/GlassCardStack.jsx +0 -111
  35. package/dist/ApexUI-Kit/templates/WaterDropReveal/WaterDropReveal.jsx +0 -151
@@ -2,18 +2,16 @@
2
2
  import React, { useState, useEffect, useRef } from 'react';
3
3
  import { motion, useMotionValue, useTransform } from 'framer-motion';
4
4
  import { ArrowLeft, ArrowRight } from 'lucide-react';
5
-
6
- // --- Card Component with Hover Parallax Effect ---
7
- const CarouselCard = ({ slide, isCenter }) => {
5
+ const CarouselCard = ({ slide, isCenter, isScrolling }) => {
8
6
  const cardRef = useRef(null);
9
7
  const x = useMotionValue(0);
10
8
  const y = useMotionValue(0);
11
9
 
12
- // Mouse move ke hisab se rotation calculate karta hai (effect ko strong banaya gaya hai)
13
10
  const rotateX = useTransform(y, [-200, 200], [15, -15]);
14
11
  const rotateY = useTransform(x, [-200, 200], [-15, 15]);
12
+
15
13
  const handleMouseMove = (event) => {
16
- if (!cardRef.current || !isCenter) return;
14
+ if (!cardRef.current || !isCenter || isScrolling) return;
17
15
  const rect = cardRef.current.getBoundingClientRect();
18
16
  x.set(event.clientX - rect.left - rect.width / 2);
19
17
  y.set(event.clientY - rect.top - rect.height / 2);
@@ -24,6 +22,13 @@ const CarouselCard = ({ slide, isCenter }) => {
24
22
  y.set(0);
25
23
  };
26
24
 
25
+ useEffect(() => {
26
+ if (isScrolling) {
27
+ x.set(0);
28
+ y.set(0);
29
+ }
30
+ }, [isScrolling, x, y]);
31
+
27
32
  return (
28
33
  <motion.div
29
34
  ref={cardRef}
@@ -36,36 +41,16 @@ const CarouselCard = ({ slide, isCenter }) => {
36
41
  }}
37
42
  onMouseMove={handleMouseMove}
38
43
  onMouseLeave={handleMouseLeave}
39
-
40
44
  >
41
45
  {/* Slide Content */}
42
- <div className="absolute inset-0 w-full h-full"
43
- style={{
44
- transformStyle: 'preserve-3d',
45
- perspective: '1500px',
46
- }}
47
- >
48
- <img
49
- src={slide.imageUrl}
50
- alt={slide.title}
51
- className="w-full h-full object-cover group-hover:opacity-30 transition-all duration-300 ease-in-out rounded-2xl"
52
- />
53
- <img
54
- src={slide.pngUrl}
55
- alt={slide.title}
56
- className="absolute inset-0 w-full h-full object-cover group-hover:translate-z-15 transition-all duration-300 ease-in-out rounded-2xl"
57
- />
46
+ <div className="absolute inset-0 w-full h-full" style={{ transformStyle: 'preserve-3d', perspective: '1500px' }}>
47
+ <img src={slide.imageUrl} alt={slide.title} className="w-full h-full object-cover group-hover:opacity-30 transition-all duration-300 ease-in-out rounded-2xl" />
48
+ <img src={slide.pngUrl} alt={slide.title} className="absolute inset-0 w-full h-full object-cover group-hover:translate-z-15 transition-all duration-300 ease-in-out rounded-2xl" />
58
49
  </div>
59
- <div
60
- className="relative flex flex-col justify-end h-full text-white p-8"
61
- style={{
62
- transformStyle: 'preserve-3d',
63
- perspective: '1500px',
64
- }}
65
- >
66
- <h2 className="text-3xl font-bold group-hover:translate-z-18 transition-all duration-300 ease-in-out">{slide.title}</h2>
50
+ <div className="relative flex flex-col justify-end h-full text-white p-5 sm:p-8" style={{ transformStyle: 'preserve-3d', perspective: '1500px' }}>
51
+ <h2 className="text-2xl sm:text-3xl font-bold group-hover:translate-z-18 transition-all duration-300 ease-in-out">{slide.title}</h2>
67
52
  <p className="text-lime-400 mt-1 group-hover:translate-z-18 transition-all duration-300 ease-in-out">{slide.subtitle}</p>
68
- <p className="text-slate-300 mt-4 text-sm leading-relaxed group-hover:translate-z-18 transition-all duration-300 ease-in-out">{slide.text}</p>
53
+ <p className="text-slate-300 mt-3 sm:mt-4 text-xs sm:text-sm leading-relaxed line-clamp-3 sm:line-clamp-none group-hover:translate-z-18 transition-all duration-300 ease-in-out">{slide.text}</p>
69
54
  </div>
70
55
  </motion.div>
71
56
  );
@@ -75,39 +60,56 @@ const CarouselCard = ({ slide, isCenter }) => {
75
60
  // --- Custom Parallax Carousel Component ---
76
61
  const ParallaxCarousel = ({ slides }) => {
77
62
  const [activeIndex, setActiveIndex] = useState(0);
78
- const [isHovered, setIsHovered] = useState(false);
63
+ const animatingRef = useRef(false);
64
+ const animTimeoutRef = useRef(null);
79
65
 
66
+ const [isScrolling, setIsScrolling] = useState(false);
67
+ const scrollTimeoutRef = useRef(null);
68
+
69
+ // ✅ REMOVED: isHovered state hata diya gaya hai.
70
+
71
+ const NAV_ANIM_MS = 600;
80
72
  const handleNext = () => {
73
+ if (animatingRef.current) return;
74
+ animatingRef.current = true;
75
+ clearTimeout(animTimeoutRef.current);
81
76
  setActiveIndex((prevIndex) => (prevIndex + 1) % slides.length);
77
+ animTimeoutRef.current = setTimeout(() => {
78
+ animatingRef.current = false;
79
+ }, NAV_ANIM_MS);
82
80
  };
83
81
 
84
82
  const handlePrev = () => {
83
+ if (animatingRef.current) return;
84
+ animatingRef.current = true;
85
+ clearTimeout(animTimeoutRef.current);
85
86
  setActiveIndex((prevIndex) => (prevIndex - 1 + slides.length) % slides.length);
87
+ animTimeoutRef.current = setTimeout(() => {
88
+ animatingRef.current = false;
89
+ }, NAV_ANIM_MS);
86
90
  };
87
-
88
- // Automatic sliding ke liye
89
91
  useEffect(() => {
90
- // Agar hovered hai to interval start hi mat karo
91
- if (isHovered) {
92
- return;
93
- }
92
+ const handleScroll = () => {
93
+ setIsScrolling(true);
94
+ clearTimeout(scrollTimeoutRef.current);
95
+ scrollTimeoutRef.current = setTimeout(() => {
96
+ setIsScrolling(false);
97
+ }, 150);
98
+ };
94
99
 
95
- const interval = setInterval(() => {
96
- handleNext();
97
- }, 4000); // Har 4 second mein slide change hoga
100
+ window.addEventListener('scroll', handleScroll, { passive: true });
101
+
102
+ return () => {
103
+ window.removeEventListener('scroll', handleScroll);
104
+ clearTimeout(scrollTimeoutRef.current);
105
+ };
106
+ }, []);
98
107
 
99
- return () => clearInterval(interval); // Cleanup
100
- }, [activeIndex, isHovered]); // isHovered state change hone par effect ko firse chalayega
101
108
 
102
109
  return (
103
- <div
104
- className="relative w-full flex flex-col items-center px-2"
105
- onMouseEnter={() => setIsHovered(true)}
106
- onMouseLeave={() => setIsHovered(false)}
107
- >
108
- {/* 3D Carousel Container */}
110
+ <div className="relative w-full flex flex-col items-center px-2">
109
111
  <div
110
- className="relative w-full h-[480px] portrait:h-[480px] portrait:px-0"
112
+ className="relative w-full h-[380px] sm:h-[480px]"
111
113
  style={{ perspective: '1200px', transformStyle: 'preserve-3d' }}
112
114
  >
113
115
  {slides.map((slide, index) => {
@@ -123,36 +125,27 @@ const ParallaxCarousel = ({ slides }) => {
123
125
  return (
124
126
  <motion.div
125
127
  key={index}
126
- className={`absolute w-full h-[340px] portrait:w-full portrait:h-[480px] top-0 left-0 portrait:left-0 portrait:top-0 ${positionOffset === 0 ? '' : ' pointer-events-none'} md:w-[50%] md:h-[400px] md:top-[50px] md:left-1/2 lg:-translate-x-1/2`}
128
+ className={`absolute left-1/2 top-1/2 aspect-[25/26] w-[min(80%,25rem)] sm:w-[min(88%,25rem)] -translate-x-1/2 -translate-y-1/2 ${positionOffset === 0 ? '' : 'pointer-events-none'}`}
127
129
  initial={false}
128
- style={{ transformStyle: 'preserve-3d' }}
130
+ style={{ transformStyle: 'preserve-3d', willChange: 'transform, opacity' }}
129
131
  animate={{
130
132
  transform: `translateX(${positionOffset * 50}%) translateZ(${-Math.abs(positionOffset) * 250}px) scale(${1 - Math.abs(positionOffset) * 0.15})`,
131
133
  opacity: isVisible ? (1 - Math.abs(positionOffset) * 0.45) : 0,
132
134
  zIndex: slides.length - Math.abs(positionOffset),
133
135
  }}
134
- transition={{ type: 'spring', stiffness: 290, damping: 30 }}
136
+ transition={{ type: 'spring', stiffness: 160, damping: 22 }}
135
137
  >
136
- <CarouselCard slide={slide} isCenter={positionOffset === 0} />
138
+ <CarouselCard slide={slide} isCenter={positionOffset === 0} isScrolling={isScrolling} />
137
139
  </motion.div>
138
140
  );
139
141
  })}
140
142
  </div>
141
143
 
142
- {/* Side Navigation Buttons */}
143
- <motion.button
144
- onClick={handlePrev}
145
- className="absolute -left-10 lg:left-0 top-1/2 -translate-y-1/2 z-30 p-3 rounded-full text-slate-400 hover:text-lime-400 transition-colors"
146
- whileTap={{ scale: 0.9 }}
147
- >
148
- <ArrowLeft size={32} />
144
+ <motion.button onClick={handlePrev} className="absolute left-0 top-1/2 -translate-y-1/2 z-30 p-1.5 sm:p-3 rounded-full text-slate-400 hover:text-lime-400 transition-colors" whileTap={{ scale: 0.9 }}>
145
+ <ArrowLeft size={32} className="h-5 w-5 sm:h-8 sm:w-8" />
149
146
  </motion.button>
150
- <motion.button
151
- onClick={handleNext}
152
- className="absolute -right-10 lg:right-0 top-1/2 -translate-y-1/2 z-30 p-3 rounded-full text-slate-400 hover:text-lime-400 transition-colors"
153
- whileTap={{ scale: 0.9 }}
154
- >
155
- <ArrowRight size={32} />
147
+ <motion.button onClick={handleNext} className="absolute right-0 top-1/2 -translate-y-1/2 z-30 p-1.5 sm:p-3 rounded-full text-slate-400 hover:text-lime-400 transition-colors" whileTap={{ scale: 0.9 }}>
148
+ <ArrowRight size={32} className="h-5 w-5 sm:h-8 sm:w-8" />
156
149
  </motion.button>
157
150
  </div>
158
151
  );
@@ -6,25 +6,24 @@ const GitHubIcon = () => (<svg width="24" height="24" fill="none" stroke="curren
6
6
  const LinkedInIcon = () => (<svg width="24" height="24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" viewBox="0 0 24 24"><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" /><rect x="2" y="9" width="4" height="12" /><circle cx="4" cy="4" r="2" /></svg>);
7
7
  const TwitterIcon = () => (<BsTwitterX size={24} />);
8
8
  const InstagramIcon = () => (<svg width="24" height="24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" viewBox="0 0 24 24"><rect x="2" y="2" width="20" height="20" rx="5" ry="5" /><path d="M16 11.37A4 4 0 1112.63 8 4 4 0 0116 11.37z" /><line x1="17.5" y1="6.5" x2="17.5" y2="6.5" /></svg>);
9
- const ParallaxProfileCard = ({ profile }) => {
10
- const defaultProfile = {
11
- avatarUrl: "/assets/spiderman.png",
12
- name: "Spider Man",
13
- username: "spider_man",
14
- dob: "10-08-2001",
15
- title: "PhotoGrapher",
16
- status: "Available for Hire",
17
- socialLinks: {
18
- github: "https://github.com/",
19
- linkedin: "https://www.linkedin.com/",
20
- twitter: "https://twitter.com/",
21
- instagram: "https://www.instagram.com/",
22
- },
23
- // Button actions
24
- onHireClick: () => alert("Hire Me button clicked!"),
25
- onCvClick: () => alert("CV button clicked!"),
26
- };
9
+ const defaultProfile = {
10
+ avatarUrl: "https://ik.imagekit.io/ny6yinyut/apexui/assets/spiderman.png?tr=f-auto,q-80,w-480",
11
+ name: "Spider Man",
12
+ username: "spider_man",
13
+ dob: "10-08-2001",
14
+ title: "PhotoGrapher",
15
+ status: "Available for Hire",
16
+ socialLinks: {
17
+ github: "https://github.com/",
18
+ linkedin: "https://www.linkedin.com/",
19
+ twitter: "https://twitter.com/",
20
+ instagram: "https://www.instagram.com/",
21
+ },
22
+ onHireClick: () => alert("Hire Me button clicked!"),
23
+ onCvClick: () => alert("CV button clicked!"),
24
+ };
27
25
 
26
+ const ParallaxProfileCard = ({ profile }) => {
28
27
  const {
29
28
  avatarUrl, name, username, dob, title, status, socialLinks, onHireClick, onCvClick
30
29
  } = profile || defaultProfile;
@@ -45,7 +44,7 @@ const ParallaxProfileCard = ({ profile }) => {
45
44
  gsap.to(card, { duration: 1, rotationX: 0, rotationY: 0, scale: 1, ease: "elastic.out(1, 0.3)" });
46
45
  };
47
46
  gsap.set(card, { transformPerspective: 1000 });
48
- card.addEventListener("mousemove", handleMouseMove);
47
+ card.addEventListener("mousemove", handleMouseMove, { passive: true });
49
48
  card.addEventListener("mouseleave", handleMouseLeave);
50
49
  return () => {
51
50
  card.removeEventListener("mousemove", handleMouseMove);
@@ -53,55 +52,70 @@ const ParallaxProfileCard = ({ profile }) => {
53
52
  };
54
53
  }, []);
55
54
 
55
+ const iconBtn = "flex h-9 w-9 items-center justify-center rounded-full border border-cyan-400/20 bg-cyan-400/5 text-cyan-300 transition-colors duration-200 hover:border-cyan-300/60 hover:text-white [&_svg]:h-[18px] [&_svg]:w-[18px]";
56
+
56
57
  return (
57
58
  <div
58
59
  ref={cardRef}
59
- className="relative w-[300px] h-[440px] bg-[#1D2B3A] rounded-3xl p-6 border border-cyan-400/30 shadow-2xl shadow-cyan-900/40 flex flex-col"
60
+ className="relative w-[300px] max-w-full bg-[#1D2B3A] rounded-3xl p-6 border border-cyan-400/30 shadow-2xl shadow-cyan-900/40"
60
61
  style={{ transformStyle: "preserve-3d" }}
61
62
  >
62
- <div className="absolute inset-0 bg-gradient-to-br from-[#1D2B3A] to-[#111827] rounded-3xl"></div>
63
- <div className="absolute top-0 left-0 w-full h-full" style={{ background: "radial-gradient(circle at 50% 50%, rgba(34, 211, 238, 0.08), transparent 70%)", transform: "translateZ(20px)" }}></div>
63
+ <div className="absolute inset-0 bg-gradient-to-br from-[#1D2B3A] to-[#0b1220] rounded-3xl"></div>
64
+ <div className="absolute inset-x-0 top-0 h-28 rounded-t-3xl" style={{ background: "radial-gradient(120% 100% at 50% 0%, rgba(34, 211, 238, 0.22), transparent 70%)", transform: "translateZ(20px)" }}></div>
64
65
 
65
- <div className="relative z-10 flex flex-col items-center justify-between h-full" style={{ transform: "translateZ(50px)" }}>
66
- {/* Top Part */}
67
- <div className="text-center">
68
- <img src={avatarUrl} alt="Profile" className="w-28 h-28 rounded-full mx-auto mb-3 border-4 border-cyan-400 shadow-lg object-cover" onError={(e) => { e.target.onerror = null; e.target.src = "https://placehold.co/128x128/1D2B3A/A5F3FC?text=ERR"; }} />
69
- <h2 className="text-2xl font-bold text-cyan-300 tracking-wide">{name}</h2>
70
- <div className="flex flex-col items-center mt-1">
71
- <span className="text-xs font-semibold text-slate-300 bg-slate-700 px-2 py-0.5 rounded-full mb-1">@{username}</span>
72
- <span className="text-xs font-medium text-cyan-400 bg-slate-700 px-2 py-0.5 rounded-full">DOB: {dob}</span>
73
- </div>
66
+ <div className="relative z-10 flex flex-col items-center text-center" style={{ transform: "translateZ(50px)" }}>
67
+ {/* Avatar with status dot */}
68
+ <div className="relative">
69
+ <img src={avatarUrl} alt="Profile" className="w-24 h-24 rounded-full border-[3px] border-cyan-400 shadow-lg shadow-cyan-500/30 object-cover" onError={(e) => { e.target.onerror = null; e.target.src = "https://placehold.co/128x128/1D2B3A/A5F3FC?text=ERR"; }} />
70
+ {status && <span className="absolute bottom-1 right-1 h-4 w-4 rounded-full border-[3px] border-[#1D2B3A] bg-green-500" />}
71
+ </div>
72
+ <h2 className="mt-3 text-2xl font-bold text-cyan-300 tracking-wide">{name}</h2>
73
+ <p className="text-sm text-slate-400">{title}</p>
74
+ <div className="mt-3 flex flex-wrap items-center justify-center gap-1.5">
75
+ <span className="text-[11px] font-semibold text-slate-300 bg-slate-700/70 px-2.5 py-1 rounded-full">@{username}</span>
76
+ <span className="text-[11px] font-medium text-cyan-300 bg-slate-700/70 px-2.5 py-1 rounded-full">DOB: {dob}</span>
74
77
  </div>
75
78
 
76
- {/* Bottom Part */}
77
- <div className="w-full text-center">
78
- <p className="text-slate-400 text-lg">{title}</p>
79
- {status && (
80
- <div className="flex items-center justify-center gap-2 mt-2">
81
- <span className="relative flex h-3 w-3"><span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-green-400 opacity-75"></span><span className="relative inline-flex rounded-full h-3 w-3 bg-green-500"></span></span>
82
- <span className="text-green-300 text-sm">{status}</span>
83
- </div>
84
- )}
85
- <div className="flex justify-center gap-4 my-4">
86
- {socialLinks?.github && (<a href={socialLinks.github} target="_blank" rel="noopener noreferrer" title="GitHub" className="text-cyan-400 hover:text-white transition-colors duration-200"><GitHubIcon /></a>)}
87
- {socialLinks?.linkedin && (<a href={socialLinks.linkedin} target="_blank" rel="noopener noreferrer" title="LinkedIn" className="text-cyan-400 hover:text-white transition-colors duration-200"><LinkedInIcon /></a>)}
88
- {socialLinks?.twitter && (<a href={socialLinks.twitter} target="_blank" rel="noopener noreferrer" title="Twitter" className="text-cyan-400 hover:text-white transition-colors duration-200"><TwitterIcon /></a>)}
89
- {socialLinks?.instagram && (<a href={socialLinks.instagram} target="_blank" rel="noopener noreferrer" title="Instagram" className="text-cyan-400 hover:text-white transition-colors duration-200"><InstagramIcon /></a>)}
79
+ {status && (
80
+ <div className="mt-4 inline-flex items-center gap-2 rounded-full border border-green-400/20 bg-green-400/5 px-3 py-1">
81
+ <span className="relative flex h-2 w-2"><span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-green-400 opacity-75"></span><span className="relative inline-flex rounded-full h-2 w-2 bg-green-500"></span></span>
82
+ <span className="text-green-300 text-xs font-medium">{status}</span>
90
83
  </div>
84
+ )}
91
85
 
92
- {/* Buttons Section */}
93
- <div className="flex justify-center gap-3">
94
- <button onClick={onHireClick} className="w-1/2 bg-cyan-500 text-slate-900 font-bold py-2 px-4 rounded-lg hover:bg-cyan-400 transition-colors duration-300">
95
- Hire Me
96
- </button>
97
- <button onClick={onCvClick} className="w-1/2 bg-slate-700 text-cyan-300 font-bold py-2 px-4 rounded-lg hover:bg-slate-600 transition-colors duration-300">
98
- CV
99
- </button>
100
- </div>
86
+ <div className="my-5 h-px w-full bg-gradient-to-r from-transparent via-cyan-400/25 to-transparent" />
87
+
88
+ <div className="flex justify-center gap-2.5">
89
+ {socialLinks?.github && (<a href={socialLinks.github} target="_blank" rel="noopener noreferrer" title="GitHub" className={iconBtn}><GitHubIcon /></a>)}
90
+ {socialLinks?.linkedin && (<a href={socialLinks.linkedin} target="_blank" rel="noopener noreferrer" title="LinkedIn" className={iconBtn}><LinkedInIcon /></a>)}
91
+ {socialLinks?.twitter && (<a href={socialLinks.twitter} target="_blank" rel="noopener noreferrer" title="Twitter" className={iconBtn}><TwitterIcon /></a>)}
92
+ {socialLinks?.instagram && (<a href={socialLinks.instagram} target="_blank" rel="noopener noreferrer" title="Instagram" className={iconBtn}><InstagramIcon /></a>)}
93
+ </div>
94
+
95
+ {/* Buttons Section */}
96
+ <div className="mt-5 flex w-full gap-3">
97
+ <button onClick={onHireClick} className="flex-1 whitespace-nowrap bg-cyan-500 text-slate-900 text-sm font-bold py-2.5 px-3 rounded-xl hover:bg-cyan-400 transition-colors duration-300">
98
+ Hire Me
99
+ </button>
100
+ <button onClick={onCvClick} className="flex-1 whitespace-nowrap bg-slate-700/80 text-cyan-300 text-sm font-bold py-2.5 px-3 rounded-xl hover:bg-slate-600 transition-colors duration-300">
101
+ Download CV
102
+ </button>
101
103
  </div>
102
104
  </div>
103
105
  </div>
104
106
  );
105
107
  };
106
108
 
107
- export default ParallaxProfileCard;
109
+ const ParallaxProfile = ({ profiles = [defaultProfile], className = '' }) => {
110
+ const safe = Array.isArray(profiles) && profiles.length > 0 ? profiles : [defaultProfile];
111
+ return (
112
+ <div className={`flex w-full flex-wrap gap-6 justify-center ${className}`}>
113
+ {safe.map((p, i) => (
114
+ <ParallaxProfileCard key={p.id || p.username || i} profile={p} />
115
+ ))}
116
+ </div>
117
+ );
118
+ };
119
+
120
+ export { ParallaxProfileCard };
121
+ export default ParallaxProfile;
@@ -1,15 +1,21 @@
1
1
  import React, { useRef, useEffect, useState } from "react";
2
2
 
3
- const people = [
4
- { id: 1, name: "Captain America", job: "Leader of the Avengers", img: "/assets/captainamerica.png" },
5
- { id: 2, name: "Doctor Strange", job: "Sorcerer Supreme", img: "/assets/doctorStrange.png" },
6
- { id: 3, name: "Iron Man", job: "Leader Of Stark Industries", img: "/assets/ironman.png" },
7
- { id: 4, name: "HULK", job: "Scientist", img: "/assets/hulk.png" },
8
- { id: 5, name: "Spider-Man", job: "Friendly Neighborhood Spider-Man", img: "/assets/spiderman.png" },
9
- { id: 6, name: "Thanos", job: "The Mad Titan", img: "/assets/thanos.png" },
3
+ const defaultPeople = [
4
+ { id: 1, name: "Captain America", job: "Leader of the Avengers", img: "https://ik.imagekit.io/ny6yinyut/apexui/assets/captainamerica.png?tr=f-auto,q-80,w-480" },
5
+ { id: 2, name: "Doctor Strange", job: "Sorcerer Supreme", img: "https://ik.imagekit.io/ny6yinyut/apexui/assets/doctorStrange.png?tr=f-auto,q-80,w-480" },
6
+ { id: 3, name: "Iron Man", job: "Leader Of Stark Industries", img: "https://ik.imagekit.io/ny6yinyut/apexui/assets/ironman.png?tr=f-auto,q-80,w-480" },
7
+ { id: 4, name: "HULK", job: "Scientist", img: "https://ik.imagekit.io/ny6yinyut/apexui/assets/hulk.png?tr=f-auto,q-80,w-480" },
8
+ { id: 5, name: "Spider-Man", job: "Friendly Neighborhood Spider-Man", img: "https://ik.imagekit.io/ny6yinyut/apexui/assets/spiderman.png?tr=f-auto,q-80,w-480" },
9
+ { id: 6, name: "Thanos", job: "The Mad Titan", img: "https://ik.imagekit.io/ny6yinyut/apexui/assets/thanos.png?tr=f-auto,q-80,w-480" },
10
10
  ];
11
11
 
12
- const PointerFollower = ({ cursorColor = "#fff", interval = 3000, badgeColor = "#fff", badgeTextColor = "#212121" }) => {
12
+ const PointerFollower = ({
13
+ cursorColor = "#fff",
14
+ interval = 3000,
15
+ badgeColor = "#fff",
16
+ badgeTextColor = "#212121",
17
+ people = defaultPeople,
18
+ }) => {
13
19
  const containerRef = useRef(null);
14
20
  const [pos, setPos] = useState({ x: 0, y: 0 });
15
21
  const [hovered, setHovered] = useState(false);
@@ -20,11 +26,14 @@ const PointerFollower = ({ cursorColor = "#fff", interval = 3000, badgeColor = "
20
26
  if (hovered) return;
21
27
 
22
28
  const autoInterval = setInterval(() => {
23
- setActiveIndex((prev) => (prev + 1) % people.length);
29
+ setActiveIndex((prev) => {
30
+ if (!people || people.length === 0) return 0;
31
+ return (prev + 1) % people.length;
32
+ });
24
33
  }, interval);
25
34
 
26
35
  return () => clearInterval(autoInterval);
27
- }, [hovered, interval]);
36
+ }, [hovered, interval, people]);
28
37
 
29
38
  // 🖱️ Mouse tracking
30
39
  useEffect(() => {
@@ -44,7 +53,7 @@ const PointerFollower = ({ cursorColor = "#fff", interval = 3000, badgeColor = "
44
53
  setHovered(false);
45
54
  };
46
55
 
47
- container.addEventListener("pointermove", handleMove);
56
+ container.addEventListener("pointermove", handleMove, { passive: true });
48
57
  container.addEventListener("pointerleave", handleLeave);
49
58
 
50
59
  return () => {
@@ -53,13 +62,24 @@ const PointerFollower = ({ cursorColor = "#fff", interval = 3000, badgeColor = "
53
62
  };
54
63
  }, []);
55
64
 
56
- const person = people[activeIndex]; // ✅ SYNCED HERE
65
+ // Guard against empty people array
66
+ const safePeople = Array.isArray(people) && people.length > 0 ? people : defaultPeople;
67
+ const person = safePeople[activeIndex % safePeople.length]; // ✅ SYNCED HERE
68
+
69
+ // If the provided people list changes, ensure activeIndex is valid
70
+ useEffect(() => {
71
+ if (!Array.isArray(people) || people.length === 0) {
72
+ setActiveIndex(0);
73
+ return;
74
+ }
75
+ setActiveIndex((idx) => idx % people.length);
76
+ }, [people]);
57
77
 
58
78
  return (
59
79
  <div className="relative w-full h-fit flex items-center justify-center">
60
80
  <div ref={containerRef} className="absolute flex items-center justify-center w-64 h-66 cursor-none">
61
81
  {/* 🔁 Image stack with auto fade */}
62
- {people.map((p, i) => (
82
+ {safePeople.map((p, i) => (
63
83
  <img
64
84
  key={i}
65
85
  src={p.img}
@@ -67,7 +87,7 @@ const PointerFollower = ({ cursorColor = "#fff", interval = 3000, badgeColor = "
67
87
  className={`absolute w-full h-full object-cover rounded-xl transition-opacity duration-1000 ease-in-out border border-white/10`}
68
88
  style={{
69
89
  transform: `translate(${i * -2}px, ${i * -2}px)`,
70
- zIndex: people.length - i,
90
+ zIndex: safePeople.length - i,
71
91
  opacity: activeIndex === i ? 1 : 0,
72
92
  }}
73
93
  />
@@ -123,10 +123,11 @@ const SmartGridCard = ({
123
123
  <div
124
124
  className={`grid gap-2 justify-center p-4 w-full ${screenWidth >= 1024 ? 'grid-cols-3' : screenWidth >= 768 ? 'grid-cols-2' : 'grid-cols-1'}`}
125
125
  style={screenWidth >= 1024 ? {
126
- gridTemplateColumns: "repeat(3, minmax(300px, 1fr))",
126
+ // minmax(0, …): columns share the container's width instead of overflowing it.
127
+ gridTemplateColumns: "repeat(3, minmax(0, 1fr))",
127
128
  gridTemplateRows: `repeat(2, ${CARD_HEIGHT}px)`
128
129
  } : screenWidth >= 768 ? {
129
- gridTemplateColumns: "repeat(2, minmax(300px, 1fr))",
130
+ gridTemplateColumns: "repeat(2, minmax(0, 1fr))",
130
131
  gridTemplateRows: `repeat(3, ${CARD_HEIGHT}px)`
131
132
  } : {}}
132
133
  >
@@ -164,7 +165,7 @@ const SmartGridCard = ({
164
165
  </div>
165
166
  <button
166
167
  type="button"
167
- className="mt-auto relative overflow-hidden px-5 py-3 rounded-lg border border-purple-400/60 text-white transition hover:text-white bg-black/20 group flex items-center gap-2 text-xs md:text-base"
168
+ className="mt-auto relative overflow-hidden whitespace-nowrap px-4 py-3 rounded-lg border border-purple-400/60 text-white transition hover:text-white bg-black/20 group flex items-center gap-2 text-xs md:text-base"
168
169
  >
169
170
  <span className="relative z-10 flex items-center gap-2">
170
171
  {card.buttonText}
@@ -1,5 +1,5 @@
1
1
  import React, { useEffect, useRef } from "react";
2
- import { Renderer, Program, Mesh, Triangle } from "https://unpkg.com/ogl";
2
+ import { Renderer, Program, Mesh, Triangle } from "ogl";
3
3
 
4
4
  const hexToRgb = (hex) => {
5
5
  const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
@@ -87,30 +87,37 @@ void main() {
87
87
 
88
88
 
89
89
  export const SmokeBackground = ({
90
- color = "#ffffff",
90
+ color = "#00c950",
91
91
  speed = 1,
92
92
  direction = "forward",
93
93
  scale = 1,
94
94
  opacity = 1,
95
95
  mouseInteractive = true,
96
96
  className = "",
97
+ maxWidth = 'max-w-4xl',
97
98
  }) => {
98
99
  const containerRef = useRef(null);
99
100
  const mousePos = useRef({ x: 0, y: 0 });
100
101
 
101
102
  useEffect(() => {
102
- if (!containerRef.current) return;
103
+ const mountNode = containerRef.current;
104
+ if (!mountNode) return;
103
105
 
104
106
  const useCustomColor = color ? 1.0 : 0.0;
105
107
  const customColorRgb = color ? hexToRgb(color) : [1, 1, 1];
106
108
 
107
109
  let directionMultiplier = direction === "backward" ? -1.0 : 1.0;
108
110
 
111
+ // cap DPR depending on mount width to avoid heavy pixel loads on small devices
112
+ const mountWidth = mountNode.clientWidth || window.innerWidth;
113
+ const dprCap = mountWidth > 1200 ? 1.8 : (mountWidth > 768 ? 1.4 : 1.0);
114
+ const dpr = Math.min(window.devicePixelRatio || 1, dprCap);
115
+
109
116
  const renderer = new Renderer({
110
117
  webgl: 2,
111
118
  alpha: true,
112
119
  antialias: false,
113
- dpr: Math.min(window.devicePixelRatio || 1, 1.8),
120
+ dpr,
114
121
  });
115
122
 
116
123
  const gl = renderer.gl;
@@ -118,7 +125,7 @@ export const SmokeBackground = ({
118
125
  canvas.style.display = "block";
119
126
  canvas.style.width = "100%";
120
127
  canvas.style.height = "100%";
121
- containerRef.current.appendChild(canvas);
128
+ mountNode.appendChild(canvas);
122
129
 
123
130
  const geometry = new Triangle(gl);
124
131
 
@@ -145,17 +152,17 @@ export const SmokeBackground = ({
145
152
  let mouseNeedsUpdate = false;
146
153
  const handleMouseMove = (e) => {
147
154
  if (!mouseInteractive) return;
148
- const rect = containerRef.current.getBoundingClientRect();
155
+ const rect = mountNode.getBoundingClientRect();
149
156
  mousePos.current.x = e.clientX - rect.left;
150
157
  mousePos.current.y = rect.height - (e.clientY - rect.top);
151
158
  mouseNeedsUpdate = true;
152
159
  };
153
160
  if (mouseInteractive) {
154
- containerRef.current.addEventListener("mousemove", handleMouseMove);
161
+ mountNode.addEventListener("mousemove", handleMouseMove, { passive: true });
155
162
  }
156
163
 
157
164
  const setSize = () => {
158
- const rect = containerRef.current.getBoundingClientRect();
165
+ const rect = mountNode.getBoundingClientRect();
159
166
  const width = Math.max(1, Math.floor(rect.width));
160
167
  const height = Math.max(1, Math.floor(rect.height));
161
168
  renderer.setSize(width, height);
@@ -167,12 +174,16 @@ export const SmokeBackground = ({
167
174
  const ro = new ResizeObserver(() => {
168
175
  requestAnimationFrame(setSize);
169
176
  });
170
- ro.observe(containerRef.current);
177
+ ro.observe(mountNode);
171
178
  setSize();
172
179
 
173
180
  let raf = 0;
181
+ // Only render while the background is on screen.
182
+ let visible = true;
174
183
  const t0 = performance.now();
175
184
  const loop = (t) => {
185
+ raf = 0;
186
+ if (!visible) return;
176
187
  let timeValue = (t - t0) * 0.001;
177
188
 
178
189
  if (direction === "funny") {
@@ -192,24 +203,32 @@ export const SmokeBackground = ({
192
203
  raf = requestAnimationFrame(loop);
193
204
  };
194
205
  raf = requestAnimationFrame(loop);
206
+ const visibilityObserver = new IntersectionObserver(([entry]) => {
207
+ visible = entry.isIntersecting;
208
+ if (visible && !raf) raf = requestAnimationFrame(loop);
209
+ });
210
+ visibilityObserver.observe(mountNode);
195
211
 
196
212
  return () => {
197
213
  cancelAnimationFrame(raf);
214
+ visibilityObserver.disconnect();
198
215
  ro.disconnect();
199
- if (mouseInteractive && containerRef.current) {
200
- containerRef.current.removeEventListener("mousemove", handleMouseMove);
216
+ if (mouseInteractive && mountNode) {
217
+ mountNode.removeEventListener("mousemove", handleMouseMove);
201
218
  }
202
219
  try {
203
- containerRef.current?.removeChild(canvas);
220
+ mountNode.removeChild(canvas);
204
221
  } catch (error) { console.error("Error removing canvas:", error); }
222
+ gl.getExtension("WEBGL_lose_context")?.loseContext();
205
223
  };
206
224
  }, [color, speed, direction, scale, opacity, mouseInteractive, className]);
207
225
 
208
226
  return (
209
- <div
210
- ref={containerRef}
211
- className={`w-screen lg:h-full h-[80vh] overflow-hidden relative ${className}`}
212
- />
227
+ <div className={`w-full ${className}`}>
228
+ <div className={`mx-auto w-full ${maxWidth} h-[100vh] lg:h-[90vh] overflow-hidden relative`}>
229
+ <div ref={containerRef} className="absolute inset-0 w-full h-full" />
230
+ </div>
231
+ </div>
213
232
  );
214
233
  };
215
234