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,241 @@
|
|
|
1
|
+
import React, { useEffect, useRef, useState } from "react";
|
|
2
|
+
import { gsap } from "gsap";
|
|
3
|
+
import {
|
|
4
|
+
Box,
|
|
5
|
+
Settings,
|
|
6
|
+
Lock,
|
|
7
|
+
Sparkles,
|
|
8
|
+
Search,
|
|
9
|
+
LaptopMinimal,
|
|
10
|
+
} from "lucide-react";
|
|
11
|
+
|
|
12
|
+
const CARD_HEIGHT = 250;
|
|
13
|
+
const CARD_EXPANDED_HEIGHT = 510;
|
|
14
|
+
|
|
15
|
+
const SmartGridCard = ({
|
|
16
|
+
cards = defaultCards(),
|
|
17
|
+
mergeMap = {}, // Example: { 5: 2 }
|
|
18
|
+
borderColor = "cyan"
|
|
19
|
+
}) => {
|
|
20
|
+
// Track screen width for responsive mergeMap logic
|
|
21
|
+
const [screenWidth, setScreenWidth] = useState(() => typeof window !== 'undefined' ? window.innerWidth : 1200);
|
|
22
|
+
useEffect(() => {
|
|
23
|
+
const handleResize = () => setScreenWidth(window.innerWidth);
|
|
24
|
+
window.addEventListener('resize', handleResize);
|
|
25
|
+
return () => window.removeEventListener('resize', handleResize);
|
|
26
|
+
}, []);
|
|
27
|
+
const isLargeScreen = screenWidth >= 1080;
|
|
28
|
+
const isThreeCol = screenWidth >= 1024;
|
|
29
|
+
const isTwoCol = screenWidth >= 768 && screenWidth < 1024;
|
|
30
|
+
const cardRefs = useRef({});
|
|
31
|
+
const [mousePos, setMousePos] = useState({});
|
|
32
|
+
|
|
33
|
+
useEffect(() => {
|
|
34
|
+
if (!isLargeScreen) return; // Only run mergeMap effect on large screens
|
|
35
|
+
// Defensive: never trigger a reload, only animate
|
|
36
|
+
if (Object.keys(mergeMap).length === 0) return; // No merges to process
|
|
37
|
+
// Animate cards based on mergeMap
|
|
38
|
+
Object.entries(mergeMap).forEach(([sourceId, targetId]) => {
|
|
39
|
+
const sourceEl = cardRefs.current[sourceId];
|
|
40
|
+
const targetEl = cardRefs.current[targetId];
|
|
41
|
+
|
|
42
|
+
if (targetEl) {
|
|
43
|
+
gsap.to(targetEl, {
|
|
44
|
+
height: CARD_EXPANDED_HEIGHT,
|
|
45
|
+
duration: 0.5,
|
|
46
|
+
ease: "power2.out",
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (sourceEl) {
|
|
51
|
+
gsap.to(sourceEl, {
|
|
52
|
+
opacity: 0,
|
|
53
|
+
scale: 0.9,
|
|
54
|
+
duration: 0.3,
|
|
55
|
+
onComplete: () => {
|
|
56
|
+
if (sourceEl) sourceEl.style.display = "none";
|
|
57
|
+
},
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
// No reload, no navigation, no window.location
|
|
62
|
+
return () => { };
|
|
63
|
+
}, [mergeMap, isLargeScreen]);
|
|
64
|
+
|
|
65
|
+
const handleMouseMove = (e, id) => {
|
|
66
|
+
const bounds = e.currentTarget.getBoundingClientRect();
|
|
67
|
+
const x = e.clientX - bounds.left;
|
|
68
|
+
const y = e.clientY - bounds.top;
|
|
69
|
+
setMousePos((prev) => ({
|
|
70
|
+
...prev,
|
|
71
|
+
[id]: { x, y },
|
|
72
|
+
}));
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const getCardStyle = (id) => {
|
|
76
|
+
// <768px: 1 column
|
|
77
|
+
if (screenWidth < 768) {
|
|
78
|
+
return {
|
|
79
|
+
display: 'block',
|
|
80
|
+
height: CARD_HEIGHT,
|
|
81
|
+
gridColumnStart: 'auto',
|
|
82
|
+
gridRowStart: 'auto',
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
// 768px - 1023px: 2 columns
|
|
86
|
+
if (isTwoCol) {
|
|
87
|
+
const col = ((id - 1) % 2) + 1;
|
|
88
|
+
const row = Math.floor((id - 1) / 2) + 1;
|
|
89
|
+
return {
|
|
90
|
+
display: 'block',
|
|
91
|
+
height: CARD_HEIGHT,
|
|
92
|
+
gridColumnStart: col,
|
|
93
|
+
gridRowStart: row,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
// 1024px and above: 3 columns (mergeMap only for >=1080px)
|
|
97
|
+
if (isThreeCol) {
|
|
98
|
+
if (screenWidth < 1080) {
|
|
99
|
+
const col = ((id - 1) % 3) + 1;
|
|
100
|
+
const row = Math.floor((id - 1) / 3) + 1;
|
|
101
|
+
return {
|
|
102
|
+
display: 'block',
|
|
103
|
+
height: CARD_HEIGHT,
|
|
104
|
+
gridColumnStart: col,
|
|
105
|
+
gridRowStart: row,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
// >=1080px: 3 columns, merging enabled
|
|
109
|
+
const isMerged = mergeMap[id];
|
|
110
|
+
const isTarget = Object.values(mergeMap).includes(id);
|
|
111
|
+
if (isMerged) return { display: "none" };
|
|
112
|
+
const col = ((id - 1) % 3) + 1;
|
|
113
|
+
const row = Math.floor((id - 1) / 3) + 1;
|
|
114
|
+
return {
|
|
115
|
+
gridColumnStart: col,
|
|
116
|
+
gridRowStart: row,
|
|
117
|
+
height: isTarget ? CARD_EXPANDED_HEIGHT : CARD_HEIGHT,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
return (
|
|
123
|
+
<div
|
|
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
|
+
style={screenWidth >= 1024 ? {
|
|
126
|
+
gridTemplateColumns: "repeat(3, minmax(300px, 1fr))",
|
|
127
|
+
gridTemplateRows: `repeat(2, ${CARD_HEIGHT}px)`
|
|
128
|
+
} : screenWidth >= 768 ? {
|
|
129
|
+
gridTemplateColumns: "repeat(2, minmax(300px, 1fr))",
|
|
130
|
+
gridTemplateRows: `repeat(3, ${CARD_HEIGHT}px)`
|
|
131
|
+
} : {}}
|
|
132
|
+
>
|
|
133
|
+
{cards.map((card) => {
|
|
134
|
+
const style = getCardStyle(card.id);
|
|
135
|
+
if (style.display === "none") return null;
|
|
136
|
+
|
|
137
|
+
const pos = mousePos[card.id] || { x: 0, y: 0 };
|
|
138
|
+
|
|
139
|
+
return (
|
|
140
|
+
<div
|
|
141
|
+
key={card.id}
|
|
142
|
+
ref={(el) => (cardRefs.current[card.id] = el)}
|
|
143
|
+
style={style}
|
|
144
|
+
onMouseMove={(e) => handleMouseMove(e, card.id)}
|
|
145
|
+
className="group relative overflow-hidden p-[1px] rounded-2xl bg-zinc-900 border border-zinc-700 shadow-2xl transition-all duration-500"
|
|
146
|
+
>
|
|
147
|
+
{/* Mouse Glow Border */}
|
|
148
|
+
<div
|
|
149
|
+
className="pointer-events-none absolute inset-0 rounded-2xl opacity-0 group-hover:opacity-100 transition duration-300"
|
|
150
|
+
style={{
|
|
151
|
+
maskImage: `radial-gradient(circle at ${pos.x}px ${pos.y}px, white 40%, transparent 80%)`,
|
|
152
|
+
WebkitMaskImage: `radial-gradient(circle at ${pos.x}px ${pos.y}px, white 40%, transparent 80%)`,
|
|
153
|
+
background: `radial-gradient(circle at ${pos.x}px ${pos.y}px, ${borderColor}, transparent 80%)`,
|
|
154
|
+
borderRadius: "1rem",
|
|
155
|
+
}}
|
|
156
|
+
/>
|
|
157
|
+
|
|
158
|
+
{/* Card Content */}
|
|
159
|
+
<div className="relative z-10 flex flex-col items-start justify-between h-full w-full p-6 rounded-2xl bg-zinc-900 text-white space-y-4">
|
|
160
|
+
<div className="text-4xl border border-dashed p-2 rounded-lg border-purple-600">{card.icon}</div>
|
|
161
|
+
<div>
|
|
162
|
+
<h3 className="text-xl md:text-xl font-bold mb-2 break-words leading-tight">{card.title}</h3>
|
|
163
|
+
<p className="text-sm md:text-base w-full text-white/70 break-words">{card.description}</p>
|
|
164
|
+
</div>
|
|
165
|
+
<button
|
|
166
|
+
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
|
+
>
|
|
169
|
+
<span className="relative z-10 flex items-center gap-2">
|
|
170
|
+
{card.buttonText}
|
|
171
|
+
<span className="arrow-icon transition-transform duration-200 group-hover:translate-x-2 flex items-center">
|
|
172
|
+
<svg width="20" height="20" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
|
|
173
|
+
<path d="M5 12h14M13 6l6 6-6 6" />
|
|
174
|
+
</svg>
|
|
175
|
+
</span>
|
|
176
|
+
</span>
|
|
177
|
+
<span
|
|
178
|
+
className="absolute bottom-0 left-0 w-full h-full opacity-0 group-hover:opacity-100 origin-bottom transition-all ease-in-out duration-500 rounded-lg"
|
|
179
|
+
style={{
|
|
180
|
+
background: "radial-gradient(circle at bottom, purple, transparent 69%)",
|
|
181
|
+
mixBlendMode: "screen",
|
|
182
|
+
}}
|
|
183
|
+
/>
|
|
184
|
+
</button>
|
|
185
|
+
</div>
|
|
186
|
+
</div>
|
|
187
|
+
);
|
|
188
|
+
})}
|
|
189
|
+
</div>
|
|
190
|
+
);
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
// Default Cards
|
|
194
|
+
function defaultCards() {
|
|
195
|
+
return [
|
|
196
|
+
{
|
|
197
|
+
id: 1,
|
|
198
|
+
icon: <LaptopMinimal className="w-6 h-6 text-white" />,
|
|
199
|
+
title: "Responsive Layout",
|
|
200
|
+
description: "Works across all screen sizes with smooth animation.",
|
|
201
|
+
buttonText: "Learn More",
|
|
202
|
+
},
|
|
203
|
+
{
|
|
204
|
+
id: 2,
|
|
205
|
+
icon: <Box className="w-6 h-6 text-white" />,
|
|
206
|
+
title: "Composable Components",
|
|
207
|
+
description: "Each card is modular and easy to customize.",
|
|
208
|
+
buttonText: "Explore",
|
|
209
|
+
},
|
|
210
|
+
{
|
|
211
|
+
id: 3,
|
|
212
|
+
icon: <Settings className="w-6 h-6 text-white" />,
|
|
213
|
+
title: "Easy Config",
|
|
214
|
+
description: "Use props like mergeMap to control logic.",
|
|
215
|
+
buttonText: "Settings",
|
|
216
|
+
},
|
|
217
|
+
{
|
|
218
|
+
id: 4,
|
|
219
|
+
icon: <Lock className="w-6 h-6 text-white" />,
|
|
220
|
+
title: "Secure UI",
|
|
221
|
+
description: "Perfect for dashboards and secure apps.",
|
|
222
|
+
buttonText: "Secure Now",
|
|
223
|
+
},
|
|
224
|
+
{
|
|
225
|
+
id: 5,
|
|
226
|
+
icon: <Sparkles className="w-6 h-6 text-white" />,
|
|
227
|
+
title: "Edge Effects",
|
|
228
|
+
description: "Glowing borders that follow your mouse.",
|
|
229
|
+
buttonText: "Try It",
|
|
230
|
+
},
|
|
231
|
+
{
|
|
232
|
+
id: 6,
|
|
233
|
+
icon: <Search className="w-6 h-6 text-white" />,
|
|
234
|
+
title: "Fast Search",
|
|
235
|
+
description: "Lightning-fast component navigation.",
|
|
236
|
+
buttonText: "Search",
|
|
237
|
+
},
|
|
238
|
+
];
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
export default SmartGridCard;
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import React, { useEffect, useRef } from "react";
|
|
2
|
+
import { Renderer, Program, Mesh, Triangle } from "https://unpkg.com/ogl";
|
|
3
|
+
|
|
4
|
+
const hexToRgb = (hex) => {
|
|
5
|
+
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
|
|
6
|
+
if (!result) return [1, 0.5, 0.2];
|
|
7
|
+
return [
|
|
8
|
+
parseInt(result[1], 16) / 255,
|
|
9
|
+
parseInt(result[2], 16) / 255,
|
|
10
|
+
parseInt(result[3], 16) / 255,
|
|
11
|
+
];
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const vertex = `#version 300 es
|
|
15
|
+
precision highp float;
|
|
16
|
+
in vec2 position;
|
|
17
|
+
in vec2 uv;
|
|
18
|
+
out vec2 vUv;
|
|
19
|
+
void main() {
|
|
20
|
+
vUv = uv;
|
|
21
|
+
gl_Position = vec4(position, 0.0, 1.0);
|
|
22
|
+
}
|
|
23
|
+
`;
|
|
24
|
+
|
|
25
|
+
const fragment = `#version 300 es
|
|
26
|
+
precision highp float;
|
|
27
|
+
uniform vec2 iResolution;
|
|
28
|
+
uniform float iTime;
|
|
29
|
+
uniform vec3 uCustomColor;
|
|
30
|
+
uniform float uUseCustomColor;
|
|
31
|
+
uniform float uSpeed;
|
|
32
|
+
uniform float uDirection;
|
|
33
|
+
uniform float uScale;
|
|
34
|
+
uniform float uOpacity;
|
|
35
|
+
uniform vec2 uMouse;
|
|
36
|
+
uniform float uMouseInteractive;
|
|
37
|
+
out vec4 fragColor;
|
|
38
|
+
|
|
39
|
+
void mainImage(out vec4 o, vec2 C) {
|
|
40
|
+
vec2 center = iResolution.xy * 0.5;
|
|
41
|
+
C = (C - center) / uScale + center;
|
|
42
|
+
|
|
43
|
+
vec2 mouseOffset = (uMouse - center) * 0.0002;
|
|
44
|
+
C += mouseOffset * length(C - center) * step(0.5, uMouseInteractive);
|
|
45
|
+
|
|
46
|
+
float i, d, z, T = iTime * uSpeed * uDirection;
|
|
47
|
+
vec3 O, p, S;
|
|
48
|
+
|
|
49
|
+
// loop count reduced to 30
|
|
50
|
+
for (vec2 r = iResolution.xy, Q; ++i < 35.; O += o.w/d*o.xyz) {
|
|
51
|
+
p = z*normalize(vec3(C-.5*r,r.y));
|
|
52
|
+
p.z -= 4.;
|
|
53
|
+
S = p;
|
|
54
|
+
d = p.y-T;
|
|
55
|
+
|
|
56
|
+
p.x += .4*(1.+p.y)*sin(d + p.x*0.1)*cos(.34*d + p.x*0.05);
|
|
57
|
+
Q = p.xz *= mat2(cos(p.y+vec4(0,11,33,0)-T));
|
|
58
|
+
z+= d = abs(sqrt(length(Q*Q)) - .25*(5.+S.y))/3.+8e-4;
|
|
59
|
+
o = 1.+sin(S.y+p.z*.5+S.z-length(S-p)+vec4(2,1,0,8));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
o.xyz = tanh(O/1e4);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
bool finite1(float x){ return !(isnan(x) || isinf(x)); }
|
|
66
|
+
vec3 sanitize(vec3 c){
|
|
67
|
+
return vec3(
|
|
68
|
+
finite1(c.r) ? c.r : 0.0,
|
|
69
|
+
finite1(c.g) ? c.g : 0.0,
|
|
70
|
+
finite1(c.b) ? c.b : 0.0
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
void main() {
|
|
75
|
+
vec4 o = vec4(0.0);
|
|
76
|
+
mainImage(o, gl_FragCoord.xy);
|
|
77
|
+
vec3 rgb = sanitize(o.rgb);
|
|
78
|
+
|
|
79
|
+
float intensity = (rgb.r + rgb.g + rgb.b) / 3.0;
|
|
80
|
+
vec3 customColor = intensity * uCustomColor;
|
|
81
|
+
vec3 finalColor = mix(rgb, customColor, step(0.5, uUseCustomColor));
|
|
82
|
+
|
|
83
|
+
float alpha = length(rgb) * uOpacity;
|
|
84
|
+
fragColor = vec4(finalColor, alpha);
|
|
85
|
+
}
|
|
86
|
+
`;
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
export const SmokeBackground = ({
|
|
90
|
+
color = "#ffffff",
|
|
91
|
+
speed = 1,
|
|
92
|
+
direction = "forward",
|
|
93
|
+
scale = 1,
|
|
94
|
+
opacity = 1,
|
|
95
|
+
mouseInteractive = true,
|
|
96
|
+
className = "",
|
|
97
|
+
}) => {
|
|
98
|
+
const containerRef = useRef(null);
|
|
99
|
+
const mousePos = useRef({ x: 0, y: 0 });
|
|
100
|
+
|
|
101
|
+
useEffect(() => {
|
|
102
|
+
if (!containerRef.current) return;
|
|
103
|
+
|
|
104
|
+
const useCustomColor = color ? 1.0 : 0.0;
|
|
105
|
+
const customColorRgb = color ? hexToRgb(color) : [1, 1, 1];
|
|
106
|
+
|
|
107
|
+
let directionMultiplier = direction === "backward" ? -1.0 : 1.0;
|
|
108
|
+
|
|
109
|
+
const renderer = new Renderer({
|
|
110
|
+
webgl: 2,
|
|
111
|
+
alpha: true,
|
|
112
|
+
antialias: false,
|
|
113
|
+
dpr: Math.min(window.devicePixelRatio || 1, 1.8),
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
const gl = renderer.gl;
|
|
117
|
+
const canvas = gl.canvas;
|
|
118
|
+
canvas.style.display = "block";
|
|
119
|
+
canvas.style.width = "100%";
|
|
120
|
+
canvas.style.height = "100%";
|
|
121
|
+
containerRef.current.appendChild(canvas);
|
|
122
|
+
|
|
123
|
+
const geometry = new Triangle(gl);
|
|
124
|
+
|
|
125
|
+
const program = new Program(gl, {
|
|
126
|
+
vertex: vertex,
|
|
127
|
+
fragment: fragment,
|
|
128
|
+
uniforms: {
|
|
129
|
+
iTime: { value: 0 },
|
|
130
|
+
iResolution: { value: new Float32Array([1, 1]) },
|
|
131
|
+
uCustomColor: { value: new Float32Array(customColorRgb) },
|
|
132
|
+
uUseCustomColor: { value: useCustomColor },
|
|
133
|
+
uSpeed: { value: speed * 0.4 },
|
|
134
|
+
uDirection: { value: directionMultiplier },
|
|
135
|
+
uScale: { value: scale },
|
|
136
|
+
uOpacity: { value: opacity },
|
|
137
|
+
uMouse: { value: new Float32Array([0, 0]) },
|
|
138
|
+
uMouseInteractive: { value: mouseInteractive ? 1.0 : 0.0 },
|
|
139
|
+
},
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
const mesh = new Mesh(gl, { geometry, program });
|
|
143
|
+
|
|
144
|
+
// 🖱 Mouse throttled to RAF
|
|
145
|
+
let mouseNeedsUpdate = false;
|
|
146
|
+
const handleMouseMove = (e) => {
|
|
147
|
+
if (!mouseInteractive) return;
|
|
148
|
+
const rect = containerRef.current.getBoundingClientRect();
|
|
149
|
+
mousePos.current.x = e.clientX - rect.left;
|
|
150
|
+
mousePos.current.y = rect.height - (e.clientY - rect.top);
|
|
151
|
+
mouseNeedsUpdate = true;
|
|
152
|
+
};
|
|
153
|
+
if (mouseInteractive) {
|
|
154
|
+
containerRef.current.addEventListener("mousemove", handleMouseMove);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const setSize = () => {
|
|
158
|
+
const rect = containerRef.current.getBoundingClientRect();
|
|
159
|
+
const width = Math.max(1, Math.floor(rect.width));
|
|
160
|
+
const height = Math.max(1, Math.floor(rect.height));
|
|
161
|
+
renderer.setSize(width, height);
|
|
162
|
+
const res = program.uniforms.iResolution.value;
|
|
163
|
+
res[0] = gl.drawingBufferWidth;
|
|
164
|
+
res[1] = gl.drawingBufferHeight;
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
const ro = new ResizeObserver(() => {
|
|
168
|
+
requestAnimationFrame(setSize);
|
|
169
|
+
});
|
|
170
|
+
ro.observe(containerRef.current);
|
|
171
|
+
setSize();
|
|
172
|
+
|
|
173
|
+
let raf = 0;
|
|
174
|
+
const t0 = performance.now();
|
|
175
|
+
const loop = (t) => {
|
|
176
|
+
let timeValue = (t - t0) * 0.001;
|
|
177
|
+
|
|
178
|
+
if (direction === "funny") {
|
|
179
|
+
const cycle = Math.sin(timeValue * 0.5);
|
|
180
|
+
program.uniforms.uDirection.value = cycle;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (mouseNeedsUpdate) {
|
|
184
|
+
const mouseUniform = program.uniforms.uMouse.value;
|
|
185
|
+
mouseUniform[0] = mousePos.current.x;
|
|
186
|
+
mouseUniform[1] = mousePos.current.y;
|
|
187
|
+
mouseNeedsUpdate = false;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
program.uniforms.iTime.value = timeValue;
|
|
191
|
+
renderer.render({ scene: mesh });
|
|
192
|
+
raf = requestAnimationFrame(loop);
|
|
193
|
+
};
|
|
194
|
+
raf = requestAnimationFrame(loop);
|
|
195
|
+
|
|
196
|
+
return () => {
|
|
197
|
+
cancelAnimationFrame(raf);
|
|
198
|
+
ro.disconnect();
|
|
199
|
+
if (mouseInteractive && containerRef.current) {
|
|
200
|
+
containerRef.current.removeEventListener("mousemove", handleMouseMove);
|
|
201
|
+
}
|
|
202
|
+
try {
|
|
203
|
+
containerRef.current?.removeChild(canvas);
|
|
204
|
+
} catch (error) { console.error("Error removing canvas:", error); }
|
|
205
|
+
};
|
|
206
|
+
}, [color, speed, direction, scale, opacity, mouseInteractive, className]);
|
|
207
|
+
|
|
208
|
+
return (
|
|
209
|
+
<div
|
|
210
|
+
ref={containerRef}
|
|
211
|
+
className={`w-screen lg:h-full h-[80vh] overflow-hidden relative ${className}`}
|
|
212
|
+
/>
|
|
213
|
+
);
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
export default SmokeBackground;
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/* eslint-disable no-unused-vars */
|
|
2
|
+
import React, { useState } from 'react';
|
|
3
|
+
import { motion, AnimatePresence } from "framer-motion";
|
|
4
|
+
import { ArrowLeft, ArrowRight } from 'lucide-react';
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
// --- Testimonials Component ---
|
|
8
|
+
const Testimonials = ({ testimonials }) => {
|
|
9
|
+
const [activeIndex, setActiveIndex] = useState(0);
|
|
10
|
+
|
|
11
|
+
const handleNext = () => {
|
|
12
|
+
setActiveIndex((prevIndex) => (prevIndex + 1) % testimonials.length);
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const handlePrev = () => {
|
|
16
|
+
setActiveIndex((prevIndex) => (prevIndex - 1 + testimonials.length) % testimonials.length);
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
// Animation variants for the text content
|
|
20
|
+
const textVariants = {
|
|
21
|
+
enter: { opacity: 0, y: 20, filter: "blur(5px)" },
|
|
22
|
+
center: { opacity: 1, y: 0, filter: "blur(0px)" },
|
|
23
|
+
exit: { opacity: 0, y: -20, filter: "blur(5px)" },
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
return (
|
|
27
|
+
<div className="w-full max-w-4xl mx-auto bg-slate-950 rounded-2xl p-8 md:p-12 border border-slate-800 grid md:grid-cols-2 gap-12 items-center overflow-hidden">
|
|
28
|
+
|
|
29
|
+
{/* Left Part: Text Content and Navigation */}
|
|
30
|
+
<div className="flex flex-col justify-center h-full text-center md:text-left order-2 md:order-1">
|
|
31
|
+
{/* Title and Subtitle with Animation */}
|
|
32
|
+
<div className="relative h-32">
|
|
33
|
+
<AnimatePresence initial={false} mode="wait">
|
|
34
|
+
<motion.div
|
|
35
|
+
key={activeIndex}
|
|
36
|
+
variants={textVariants}
|
|
37
|
+
initial="enter"
|
|
38
|
+
animate="center"
|
|
39
|
+
exit="exit"
|
|
40
|
+
transition={{ duration: 0.4, ease: "easeInOut" }}
|
|
41
|
+
className="absolute w-full"
|
|
42
|
+
>
|
|
43
|
+
<h2 className="text-3xl font-bold text-white tracking-tight">{testimonials[activeIndex].author}</h2>
|
|
44
|
+
<p className="mt-2 text-lime-400 font-medium">{testimonials[activeIndex].title}</p>
|
|
45
|
+
<p className="mt-4 text-slate-400 text-sm leading-relaxed hidden md:block">"{testimonials[activeIndex].text}"</p>
|
|
46
|
+
</motion.div>
|
|
47
|
+
</AnimatePresence>
|
|
48
|
+
</div>
|
|
49
|
+
|
|
50
|
+
{/* Navigation Buttons with Microinteractions */}
|
|
51
|
+
<div className="flex items-center justify-center md:justify-start gap-4 mt-8">
|
|
52
|
+
<motion.button
|
|
53
|
+
onClick={handlePrev}
|
|
54
|
+
className="bg-slate-800/80 hover:bg-slate-800 rounded-full p-3 transition-colors duration-300 text-slate-400 hover:text-lime-400 focus:outline-none"
|
|
55
|
+
whileHover={{ scale: 1.1 }}
|
|
56
|
+
whileTap={{ scale: 0.95 }}
|
|
57
|
+
transition={{ type: 'spring', stiffness: 400, damping: 17 }}
|
|
58
|
+
>
|
|
59
|
+
<ArrowLeft size={20} />
|
|
60
|
+
</motion.button>
|
|
61
|
+
<motion.button
|
|
62
|
+
onClick={handleNext}
|
|
63
|
+
className="bg-slate-800/80 hover:bg-slate-800 rounded-full p-3 transition-colors duration-300 text-slate-400 hover:text-lime-400 focus:outline-none"
|
|
64
|
+
whileHover={{ scale: 1.1 }}
|
|
65
|
+
whileTap={{ scale: 0.95 }}
|
|
66
|
+
transition={{ type: 'spring', stiffness: 400, damping: 17 }}
|
|
67
|
+
>
|
|
68
|
+
<ArrowRight size={20} />
|
|
69
|
+
</motion.button>
|
|
70
|
+
</div>
|
|
71
|
+
</div>
|
|
72
|
+
|
|
73
|
+
{/* Right Part: Stacked Images */}
|
|
74
|
+
<div className="relative h-80 w-full flex items-center justify-center order-1 md:order-2">
|
|
75
|
+
{testimonials.map((testimonial, index) => {
|
|
76
|
+
// Determine the position of the card in the stack
|
|
77
|
+
const position = (index - activeIndex + testimonials.length) % testimonials.length;
|
|
78
|
+
|
|
79
|
+
// Only render the top 4 cards for performance and visual clarity
|
|
80
|
+
if (position > 3) return null;
|
|
81
|
+
|
|
82
|
+
return (
|
|
83
|
+
<motion.div
|
|
84
|
+
key={testimonial.id || index}
|
|
85
|
+
className="absolute w-64 h-80 bg-slate-800 rounded-xl shadow-2xl overflow-hidden border-2 border-slate-700"
|
|
86
|
+
initial={false}
|
|
87
|
+
animate={{
|
|
88
|
+
scale: 1 - position * 0.08,
|
|
89
|
+
rotate: -4 + position * 4, // Add a fanned rotation effect
|
|
90
|
+
left: `${position * 20}px`, // Fan out horizontally
|
|
91
|
+
top: `${position * 10}px`, // Stagger vertically
|
|
92
|
+
zIndex: testimonials.length - position,
|
|
93
|
+
opacity: position === 0 ? 1 : 0.5, // Only top image is fully opaque
|
|
94
|
+
}}
|
|
95
|
+
transition={{
|
|
96
|
+
type: 'spring',
|
|
97
|
+
stiffness: 260,
|
|
98
|
+
damping: 25,
|
|
99
|
+
}}
|
|
100
|
+
>
|
|
101
|
+
<img
|
|
102
|
+
src={testimonial.image}
|
|
103
|
+
alt={testimonial.author}
|
|
104
|
+
className="w-full h-full object-cover"
|
|
105
|
+
onError={(e) => { e.target.onerror = null; e.target.src = `https://placehold.co/256x320/0f172a/a3e635?text=${testimonial.author.split(' ')[0]}`; }}
|
|
106
|
+
/>
|
|
107
|
+
</motion.div>
|
|
108
|
+
);
|
|
109
|
+
})}
|
|
110
|
+
</div>
|
|
111
|
+
</div>
|
|
112
|
+
);
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
export default Testimonials;
|