diff --git a/apps/web-dashboard/package.json b/apps/web-dashboard/package.json index d396d13a9..38c6abb56 100644 --- a/apps/web-dashboard/package.json +++ b/apps/web-dashboard/package.json @@ -19,7 +19,9 @@ "@tanstack/react-table": "^8.21.3", "axios": "^1.13.2", "framer-motion": "^12.35.0", + "gsap": "^3.15.0", "lucide-react": "^0.554.0", + "postprocessing": "^6.39.1", "react": "^19.2.0", "react-dom": "^19.2.0", "react-hot-toast": "^2.6.0", @@ -27,7 +29,8 @@ "react-router-dom": "^7.9.6", "recharts": "^2.13.0", "remark-gfm": "^4.0.1", - "tailwindcss": "^4.1.18" + "tailwindcss": "^4.1.18", + "three": "^0.184.0" }, "devDependencies": { "@eslint/js": "^9.39.1", diff --git a/apps/web-dashboard/src/components/AuthShell.jsx b/apps/web-dashboard/src/components/AuthShell.jsx index 708b16817..1559c2bfe 100644 --- a/apps/web-dashboard/src/components/AuthShell.jsx +++ b/apps/web-dashboard/src/components/AuthShell.jsx @@ -17,18 +17,25 @@ function AuthShell({
+
+
U
+ urBackend +
+
- {onModeClick ? ( - - ) : ( - {modeLabel} + {modeLabel && ( + onModeClick ? ( + + ) : ( + {modeLabel} + ) )} {title ?

{title}

: null} {subtitle ?

{subtitle}

: null} diff --git a/apps/web-dashboard/src/components/BorderGlow/BorderGlow.css b/apps/web-dashboard/src/components/BorderGlow/BorderGlow.css new file mode 100644 index 000000000..9ef6848f4 --- /dev/null +++ b/apps/web-dashboard/src/components/BorderGlow/BorderGlow.css @@ -0,0 +1,106 @@ +.border-glow-card { + --edge-proximity: 0; + --cursor-angle: 45deg; + --edge-sensitivity: 30; + --color-sensitivity: calc(var(--edge-sensitivity) + 20); + --border-radius: 28px; + --glow-padding: 40px; + --cone-spread: 25; + + position: relative; + border-radius: var(--border-radius); + isolation: isolate; + transform: translate3d(0, 0, 0.01px); + display: flex; + flex-direction: column; + border: 1px solid rgba(255, 255, 255, 0.08); + background: var(--card-bg, rgba(255, 255, 255, 0.02)); + backdrop-filter: blur(12px); + -webkit-backdrop-filter: blur(12px); + overflow: visible; + box-shadow: + rgba(0, 0, 0, 0.1) 0px 1px 2px, + rgba(0, 0, 0, 0.1) 0px 2px 4px, + rgba(0, 0, 0, 0.1) 0px 4px 8px, + rgba(0, 0, 0, 0.1) 0px 8px 16px, + rgba(0, 0, 0, 0.1) 0px 16px 32px, + rgba(0, 0, 0, 0.1) 0px 32px 64px; +} + +.border-glow-card::before, +.border-glow-card > .edge-light { + content: ""; + position: absolute; + inset: 0; + border-radius: inherit; + transition: opacity 0.25s ease-out; + z-index: -1; +} + +.border-glow-card:not(:hover):not(.sweep-active)::before, +.border-glow-card:not(:hover):not(.sweep-active) > .edge-light { + opacity: 0; + transition: opacity 0.75s ease-in-out; +} + +/* colored mesh-gradient border */ +.border-glow-card::before { + border: 1px solid transparent; + background: + linear-gradient(#0c0a0f 0 100%) padding-box, + linear-gradient(rgba(255, 255, 255, 0) 0% 100%) border-box, + var(--gradient-one, radial-gradient(at 80% 55%, hsla(268, 100%, 76%, 1) 0px, transparent 50%)) border-box, + var(--gradient-two, radial-gradient(at 69% 34%, hsla(349, 100%, 74%, 1) 0px, transparent 50%)) border-box, + var(--gradient-three, radial-gradient(at 8% 6%, hsla(136, 100%, 78%, 1) 0px, transparent 50%)) border-box, + var(--gradient-four, radial-gradient(at 41% 38%, hsla(192, 100%, 64%, 1) 0px, transparent 50%)) border-box, + var(--gradient-five, radial-gradient(at 86% 85%, hsla(186, 100%, 74%, 1) 0px, transparent 50%)) border-box, + var(--gradient-six, radial-gradient(at 82% 18%, hsla(52, 100%, 65%, 1) 0px, transparent 50%)) border-box, + var(--gradient-seven, radial-gradient(at 51% 4%, hsla(12, 100%, 72%, 1) 0px, transparent 50%)) border-box, + var(--gradient-base, linear-gradient(#c299ff 0 100%)) border-box; + opacity: calc((var(--edge-proximity) - var(--color-sensitivity)) / (100 - var(--color-sensitivity))); + mask-image: + conic-gradient( + from var(--cursor-angle) at center, + black calc(var(--cone-spread) * 1%), + transparent calc((var(--cone-spread) + 15) * 1%), + transparent calc((100 - var(--cone-spread) - 15) * 1%), + black calc((100 - var(--cone-spread)) * 1%) + ); +} + +/* outer glow layer */ +.border-glow-card > .edge-light { + inset: calc(var(--glow-padding) * -1); + pointer-events: none; + z-index: 1; + mask-image: + conic-gradient( + from var(--cursor-angle) at center, black 2.5%, transparent 10%, transparent 90%, black 97.5% + ); + opacity: calc((var(--edge-proximity) - var(--edge-sensitivity)) / (100 - var(--edge-sensitivity))); + mix-blend-mode: plus-lighter; +} + +.border-glow-card > .edge-light::before { + content: ""; + position: absolute; + inset: var(--glow-padding); + border-radius: inherit; + box-shadow: + 0 0 1px 0 var(--glow-color-60, hsl(40deg 80% 80% / 60%)), + 0 0 3px 0 var(--glow-color-50, hsl(40deg 80% 80% / 50%)), + 0 0 6px 0 var(--glow-color-40, hsl(40deg 80% 80% / 40%)), + 0 0 15px 0 var(--glow-color-30, hsl(40deg 80% 80% / 30%)), + 0 0 25px 2px var(--glow-color-20, hsl(40deg 80% 80% / 20%)), + 0 0 50px 2px var(--glow-color-10, hsl(40deg 80% 80% / 10%)); +} + +.border-glow-inner { + display: flex; + flex-direction: column; + justify-content: space-between; + height: 100%; + width: 100%; + position: relative; + z-index: 1; +} diff --git a/apps/web-dashboard/src/components/BorderGlow/BorderGlow.jsx b/apps/web-dashboard/src/components/BorderGlow/BorderGlow.jsx new file mode 100644 index 000000000..f901ccd3c --- /dev/null +++ b/apps/web-dashboard/src/components/BorderGlow/BorderGlow.jsx @@ -0,0 +1,170 @@ +import { useRef, useCallback, useEffect } from 'react'; +import './BorderGlow.css'; + +function parseHSL(hslStr) { + const match = hslStr.match(/([\d.]+)\s*([\d.]+)%?\s*([\d.]+)%?/); + if (!match) return { h: 40, s: 80, l: 80 }; + return { h: parseFloat(match[1]), s: parseFloat(match[2]), l: parseFloat(match[3]) }; +} + +function buildGlowVars(glowColor, intensity) { + const { h, s, l } = parseHSL(glowColor); + const base = `${h}deg ${s}% ${l}%`; + const opacities = [100, 60, 50, 40, 30, 20, 10]; + const keys = ['', '-60', '-50', '-40', '-30', '-20', '-10']; + const vars = {}; + for (let i = 0; i < opacities.length; i++) { + vars[`--glow-color${keys[i]}`] = `hsl(${base} / ${Math.min(opacities[i] * intensity, 100)}%)`; + } + return vars; +} + +const GRADIENT_POSITIONS = ['80% 55%', '69% 34%', '8% 6%', '41% 38%', '86% 85%', '82% 18%', '51% 4%']; +const GRADIENT_KEYS = ['--gradient-one', '--gradient-two', '--gradient-three', '--gradient-four', '--gradient-five', '--gradient-six', '--gradient-seven']; +const COLOR_MAP = [0, 1, 2, 0, 1, 2, 1]; + +function buildGradientVars(colors) { + const vars = {}; + for (let i = 0; i < 7; i++) { + const c = colors[Math.min(COLOR_MAP[i], colors.length - 1)]; + vars[GRADIENT_KEYS[i]] = `radial-gradient(at ${GRADIENT_POSITIONS[i]}, ${c} 0px, transparent 50%)`; + } + vars['--gradient-base'] = `linear-gradient(${colors[0]} 0 100%)`; + return vars; +} + +function easeOutCubic(x) { return 1 - Math.pow(1 - x, 3); } +function easeInCubic(x) { return x * x * x; } + +function animateValue({ start = 0, end = 100, duration = 1000, delay = 0, ease = easeOutCubic, onUpdate, onEnd }) { + const t0 = performance.now() + delay; + let rafId = null; + function tick() { + const elapsed = performance.now() - t0; + const t = Math.min(elapsed / duration, 1); + onUpdate(start + (end - start) * ease(t)); + if (t < 1) rafId = requestAnimationFrame(tick); + else if (onEnd) onEnd(); + } + const timeoutId = setTimeout(() => { + rafId = requestAnimationFrame(tick); + }, delay); + + return () => { + clearTimeout(timeoutId); + if (rafId != null) cancelAnimationFrame(rafId); + }; +} + +const BorderGlow = ({ + children, + className = '', + edgeSensitivity = 30, + glowColor = '40 80 80', + backgroundColor = 'rgba(255, 255, 255, 0.02)', + borderRadius = 16, + glowRadius = 40, + glowIntensity = 1.0, + coneSpread = 25, + animated = false, + colors = ['#00f5d4', '#00b4ab', '#00ffd8'], + fillOpacity = 0.5, +}) => { + const cardRef = useRef(null); + + const getCenterOfElement = useCallback((el) => { + const { width, height } = el.getBoundingClientRect(); + return [width / 2, height / 2]; + }, []); + + const getEdgeProximity = useCallback((el, x, y) => { + const [cx, cy] = getCenterOfElement(el); + const dx = x - cx; + const dy = y - cy; + let kx = Infinity; + let ky = Infinity; + if (dx !== 0) kx = cx / Math.abs(dx); + if (dy !== 0) ky = cy / Math.abs(dy); + return Math.min(Math.max(1 / Math.min(kx, ky), 0), 1); + }, [getCenterOfElement]); + + const getCursorAngle = useCallback((el, x, y) => { + const [cx, cy] = getCenterOfElement(el); + const dx = x - cx; + const dy = y - cy; + if (dx === 0 && dy === 0) return 0; + const radians = Math.atan2(dy, dx); + let degrees = radians * (180 / Math.PI) + 90; + if (degrees < 0) degrees += 360; + return degrees; + }, [getCenterOfElement]); + + const handlePointerMove = useCallback((e) => { + const card = cardRef.current; + if (!card) return; + + const rect = card.getBoundingClientRect(); + const x = e.clientX - rect.left; + const y = e.clientY - rect.top; + + const edge = getEdgeProximity(card, x, y); + const angle = getCursorAngle(card, x, y); + + card.style.setProperty('--edge-proximity', `${(edge * 100).toFixed(3)}`); + card.style.setProperty('--cursor-angle', `${angle.toFixed(3)}deg`); + }, [getEdgeProximity, getCursorAngle]); + + useEffect(() => { + if (!animated || !cardRef.current) return; + const card = cardRef.current; + const angleStart = 110; + const angleEnd = 465; + card.classList.add('sweep-active'); + card.style.setProperty('--cursor-angle', `${angleStart}deg`); + + const stops = []; + stops.push(animateValue({ duration: 500, onUpdate: v => card.style.setProperty('--edge-proximity', v) })); + stops.push(animateValue({ ease: easeInCubic, duration: 1500, end: 50, onUpdate: v => { + card.style.setProperty('--cursor-angle', `${(angleEnd - angleStart) * (v / 100) + angleStart}deg`); + }})); + stops.push(animateValue({ ease: easeOutCubic, delay: 1500, duration: 2250, start: 50, end: 100, onUpdate: v => { + card.style.setProperty('--cursor-angle', `${(angleEnd - angleStart) * (v / 100) + angleStart}deg`); + }})); + stops.push(animateValue({ ease: easeInCubic, delay: 2500, duration: 1500, start: 100, end: 0, + onUpdate: v => card.style.setProperty('--edge-proximity', v), + onEnd: () => card.classList.remove('sweep-active'), + })); + + return () => { + stops.forEach(stop => stop?.()); + card.classList.remove('sweep-active'); + }; + }, [animated]); + + const glowVars = buildGlowVars(glowColor, glowIntensity); + + return ( +
+ +
+ {children} +
+
+ ); +}; + +export default BorderGlow; diff --git a/apps/web-dashboard/src/components/Hyperspeed/Hyperspeed.css b/apps/web-dashboard/src/components/Hyperspeed/Hyperspeed.css new file mode 100644 index 000000000..b76bf9b0f --- /dev/null +++ b/apps/web-dashboard/src/components/Hyperspeed/Hyperspeed.css @@ -0,0 +1,17 @@ +#lights { + width: 100%; + height: 100%; + overflow: hidden; + position: absolute; + top: 0; + left: 0; + z-index: 0; /* Behind all content in the hero section! */ + pointer-events: none; /* Make sure users can click elements over it */ + opacity: 0.45; /* Soften the light beams for better foreground text readability */ +} + +#lights canvas { + width: 100%; + height: 100%; + display: block; +} diff --git a/apps/web-dashboard/src/components/Hyperspeed/Hyperspeed.jsx b/apps/web-dashboard/src/components/Hyperspeed/Hyperspeed.jsx new file mode 100644 index 000000000..093068875 --- /dev/null +++ b/apps/web-dashboard/src/components/Hyperspeed/Hyperspeed.jsx @@ -0,0 +1,1189 @@ +import { BloomEffect, EffectComposer, EffectPass, RenderPass, SMAAEffect, SMAAPreset } from 'postprocessing'; +import { useEffect, useRef } from 'react'; +import * as THREE from 'three'; + +import './Hyperspeed.css'; + +const DEFAULT_EFFECT_OPTIONS = { + onSpeedUp: () => {}, + onSlowDown: () => {}, + distortion: 'turbulentDistortion', + length: 400, + roadWidth: 10, + islandWidth: 2, + lanesPerRoad: 4, + fov: 90, + fovSpeedUp: 150, + speedUp: 2, + carLightsFade: 0.4, + totalSideLightSticks: 20, + lightPairsPerRoadWay: 40, + shoulderLinesWidthPercentage: 0.05, + brokenLinesWidthPercentage: 0.1, + brokenLinesLengthPercentage: 0.5, + lightStickWidth: [0.12, 0.5], + lightStickHeight: [1.3, 1.7], + movingAwaySpeed: [60, 80], + movingCloserSpeed: [-120, -160], + carLightsLength: [400 * 0.03, 400 * 0.2], + carLightsRadius: [0.05, 0.14], + carWidthPercentage: [0.3, 0.5], + carShiftX: [-0.8, 0.8], + carFloorSeparation: [0, 5], + colors: { + roadColor: 0x080808, + islandColor: 0x0a0a0a, + background: 0x000000, + shoulderLines: 0xffffff, + brokenLines: 0xffffff, + leftCars: [0xd856bf, 0x6750a2, 0xc247ac], + rightCars: [0x03b3c3, 0x0e5ea5, 0x324555], + sticks: 0x03b3c3 + } +}; + +const Hyperspeed = ({ effectOptions = DEFAULT_EFFECT_OPTIONS }) => { + const hyperspeed = useRef(null); + const appRef = useRef(null); + + useEffect(() => { + if (appRef.current) { + appRef.current.dispose(); + appRef.current = null; + const container = hyperspeed.current; + if (container) { + while (container.firstChild) { + container.removeChild(container.firstChild); + } + } + } + const mountainUniforms = { + uFreq: { value: new THREE.Vector3(3, 6, 10) }, + uAmp: { value: new THREE.Vector3(30, 30, 20) } + }; + + const xyUniforms = { + uFreq: { value: new THREE.Vector2(5, 2) }, + uAmp: { value: new THREE.Vector2(25, 15) } + }; + + const LongRaceUniforms = { + uFreq: { value: new THREE.Vector2(2, 3) }, + uAmp: { value: new THREE.Vector2(35, 10) } + }; + + const turbulentUniforms = { + uFreq: { value: new THREE.Vector4(4, 8, 8, 1) }, + uAmp: { value: new THREE.Vector4(25, 5, 10, 10) } + }; + + const deepUniforms = { + uFreq: { value: new THREE.Vector2(4, 8) }, + uAmp: { value: new THREE.Vector2(10, 20) }, + uPowY: { value: new THREE.Vector2(20, 2) } + }; + + let nsin = val => Math.sin(val) * 0.5 + 0.5; + + const distortions = { + mountainDistortion: { + uniforms: mountainUniforms, + getDistortion: ` + uniform vec3 uAmp; + uniform vec3 uFreq; + #define PI 3.14159265358979 + float nsin(float val){ + return sin(val) * 0.5 + 0.5; + } + vec3 getDistortion(float progress){ + float movementProgressFix = 0.02; + return vec3( + cos(progress * PI * uFreq.x + uTime) * uAmp.x - cos(movementProgressFix * PI * uFreq.x + uTime) * uAmp.x, + nsin(progress * PI * uFreq.y + uTime) * uAmp.y - nsin(movementProgressFix * PI * uFreq.y + uTime) * uAmp.y, + nsin(progress * PI * uFreq.z + uTime) * uAmp.z - nsin(movementProgressFix * PI * uFreq.z + uTime) * uAmp.z + ); + } + `, + getJS: (progress, time) => { + let movementProgressFix = 0.02; + let uFreq = mountainUniforms.uFreq.value; + let uAmp = mountainUniforms.uAmp.value; + let distortion = new THREE.Vector3( + Math.cos(progress * Math.PI * uFreq.x + time) * uAmp.x - + Math.cos(movementProgressFix * Math.PI * uFreq.x + time) * uAmp.x, + nsin(progress * Math.PI * uFreq.y + time) * uAmp.y - + nsin(movementProgressFix * Math.PI * uFreq.y + time) * uAmp.y, + nsin(progress * Math.PI * uFreq.z + time) * uAmp.z - + nsin(movementProgressFix * Math.PI * uFreq.z + time) * uAmp.z + ); + let lookAtAmp = new THREE.Vector3(2, 2, 2); + let lookAtOffset = new THREE.Vector3(0, 0, -5); + return distortion.multiply(lookAtAmp).add(lookAtOffset); + } + }, + xyDistortion: { + uniforms: xyUniforms, + getDistortion: ` + uniform vec2 uFreq; + uniform vec2 uAmp; + #define PI 3.14159265358979 + vec3 getDistortion(float progress){ + float movementProgressFix = 0.02; + return vec3( + cos(progress * PI * uFreq.x + uTime) * uAmp.x - cos(movementProgressFix * PI * uFreq.x + uTime) * uAmp.x, + sin(progress * PI * uFreq.y + PI/2. + uTime) * uAmp.y - sin(movementProgressFix * PI * uFreq.y + PI/2. + uTime) * uAmp.y, + 0. + ); + } + `, + getJS: (progress, time) => { + let movementProgressFix = 0.02; + let uFreq = xyUniforms.uFreq.value; + let uAmp = xyUniforms.uAmp.value; + let distortion = new THREE.Vector3( + Math.cos(progress * Math.PI * uFreq.x + time) * uAmp.x - + Math.cos(movementProgressFix * Math.PI * uFreq.x + time) * uAmp.x, + Math.sin(progress * Math.PI * uFreq.y + time + Math.PI / 2) * uAmp.y - + Math.sin(movementProgressFix * Math.PI * uFreq.y + time + Math.PI / 2) * uAmp.y, + 0 + ); + let lookAtAmp = new THREE.Vector3(2, 0.4, 1); + let lookAtOffset = new THREE.Vector3(0, 0, -3); + return distortion.multiply(lookAtAmp).add(lookAtOffset); + } + }, + LongRaceDistortion: { + uniforms: LongRaceUniforms, + getDistortion: ` + uniform vec2 uFreq; + uniform vec2 uAmp; + #define PI 3.14159265358979 + vec3 getDistortion(float progress){ + float camProgress = 0.0125; + return vec3( + sin(progress * PI * uFreq.x + uTime) * uAmp.x - sin(camProgress * PI * uFreq.x + uTime) * uAmp.x, + sin(progress * PI * uFreq.y + uTime) * uAmp.y - sin(camProgress * PI * uFreq.y + uTime) * uAmp.y, + 0. + ); + } + `, + getJS: (progress, time) => { + let camProgress = 0.0125; + let uFreq = LongRaceUniforms.uFreq.value; + let uAmp = LongRaceUniforms.uAmp.value; + let distortion = new THREE.Vector3( + Math.sin(progress * Math.PI * uFreq.x + time) * uAmp.x - + Math.sin(camProgress * Math.PI * uFreq.x + time) * uAmp.x, + Math.sin(progress * Math.PI * uFreq.y + time) * uAmp.y - + Math.sin(camProgress * Math.PI * uFreq.y + time) * uAmp.y, + 0 + ); + let lookAtAmp = new THREE.Vector3(1, 1, 0); + let lookAtOffset = new THREE.Vector3(0, 0, -5); + return distortion.multiply(lookAtAmp).add(lookAtOffset); + } + }, + turbulentDistortion: { + uniforms: turbulentUniforms, + getDistortion: ` + uniform vec4 uFreq; + uniform vec4 uAmp; + float nsin(float val){ + return sin(val) * 0.5 + 0.5; + } + #define PI 3.14159265358979 + float getDistortionX(float progress){ + return ( + cos(PI * progress * uFreq.r + uTime) * uAmp.r + + pow(cos(PI * progress * uFreq.g + uTime * (uFreq.g / uFreq.r)), 2. ) * uAmp.g + ); + } + float getDistortionY(float progress){ + return ( + -nsin(PI * progress * uFreq.b + uTime) * uAmp.b + + -pow(nsin(PI * progress * uFreq.a + uTime / (uFreq.b / uFreq.a)), 5.) * uAmp.a + ); + } + vec3 getDistortion(float progress){ + return vec3( + getDistortionX(progress) - getDistortionX(0.0125), + getDistortionY(progress) - getDistortionY(0.0125), + 0. + ); + } + `, + getJS: (progress, time) => { + const uFreq = turbulentUniforms.uFreq.value; + const uAmp = turbulentUniforms.uAmp.value; + + const getX = p => + Math.cos(Math.PI * p * uFreq.x + time) * uAmp.x + + Math.pow(Math.cos(Math.PI * p * uFreq.y + time * (uFreq.y / uFreq.x)), 2) * uAmp.y; + + const getY = p => + -nsin(Math.PI * p * uFreq.z + time) * uAmp.z - + Math.pow(nsin(Math.PI * p * uFreq.w + time / (uFreq.z / uFreq.w)), 5) * uAmp.w; + + let distortion = new THREE.Vector3( + getX(progress) - getX(progress + 0.007), + getY(progress) - getY(progress + 0.007), + 0 + ); + let lookAtAmp = new THREE.Vector3(-2, -5, 0); + let lookAtOffset = new THREE.Vector3(0, 0, -10); + return distortion.multiply(lookAtAmp).add(lookAtOffset); + } + }, + turbulentDistortionStill: { + uniforms: turbulentUniforms, + getDistortion: ` + uniform vec4 uFreq; + uniform vec4 uAmp; + float nsin(float val){ + return sin(val) * 0.5 + 0.5; + } + #define PI 3.14159265358979 + float getDistortionX(float progress){ + return ( + cos(PI * progress * uFreq.r) * uAmp.r + + pow(cos(PI * progress * uFreq.g * (uFreq.g / uFreq.r)), 2. ) * uAmp.g + ); + } + float getDistortionY(float progress){ + return ( + -nsin(PI * progress * uFreq.b) * uAmp.b + + -pow(nsin(PI * progress * uFreq.a / (uFreq.b / uFreq.a)), 5.) * uAmp.a + ); + } + vec3 getDistortion(float progress){ + return vec3( + getDistortionX(progress) - getDistortionX(0.02), + getDistortionY(progress) - getDistortionY(0.02), + 0. + ); + } + ` + }, + deepDistortionStill: { + uniforms: deepUniforms, + getDistortion: ` + uniform vec4 uFreq; + uniform vec4 uAmp; + uniform vec2 uPowY; + float nsin(float val){ + return sin(val) * 0.5 + 0.5; + } + #define PI 3.14159265358979 + float getDistortionX(float progress){ + return ( + sin(progress * PI * uFreq.x) * uAmp.x * 2. + ); + } + float getDistortionY(float progress){ + return ( + pow(abs(progress * uPowY.x), uPowY.y) + sin(progress * PI * uFreq.y) * uAmp.y + ); + } + vec3 getDistortion(float progress){ + return vec3( + getDistortionX(progress) - getDistortionX(0.02), + getDistortionY(progress) - getDistortionY(0.05), + 0. + ); + } + ` + }, + deepDistortion: { + uniforms: deepUniforms, + getDistortion: ` + uniform vec4 uFreq; + uniform vec4 uAmp; + uniform vec2 uPowY; + float nsin(float val){ + return sin(val) * 0.5 + 0.5; + } + #define PI 3.14159265358979 + float getDistortionX(float progress){ + return ( + sin(progress * PI * uFreq.x + uTime) * uAmp.x + ); + } + float getDistortionY(float progress){ + return ( + pow(abs(progress * uPowY.x), uPowY.y) + sin(progress * PI * uFreq.y + uTime) * uAmp.y + ); + } + vec3 getDistortion(float progress){ + return vec3( + getDistortionX(progress) - getDistortionX(0.02), + getDistortionY(progress) - getDistortionY(0.02), + 0. + ); + } + `, + getJS: (progress, time) => { + const uFreq = deepUniforms.uFreq.value; + const uAmp = deepUniforms.uAmp.value; + const uPowY = deepUniforms.uPowY.value; + + const getX = p => Math.sin(p * Math.PI * uFreq.x + time) * uAmp.x; + const getY = p => Math.pow(p * uPowY.x, uPowY.y) + Math.sin(p * Math.PI * uFreq.y + time) * uAmp.y; + + let distortion = new THREE.Vector3( + getX(progress) - getX(progress + 0.01), + getY(progress) - getY(progress + 0.01), + 0 + ); + let lookAtAmp = new THREE.Vector3(-2, -4, 0); + let lookAtOffset = new THREE.Vector3(0, 0, -10); + return distortion.multiply(lookAtAmp).add(lookAtOffset); + } + } + }; + + class App { + constructor(container, options = {}) { + this.options = options; + if (this.options.distortion == null) { + this.options.distortion = { + uniforms: distortion_uniforms, + getDistortion: distortion_vertex + }; + } + this.container = container; + this.hasValidSize = false; + + const initW = Math.max(1, container.offsetWidth); + const initH = Math.max(1, container.offsetHeight); + + this.renderer = new THREE.WebGLRenderer({ + antialias: false, + alpha: true + }); + this.renderer.setSize(initW, initH, false); + this.renderer.setPixelRatio(window.devicePixelRatio); + this.composer = new EffectComposer(this.renderer); + container.append(this.renderer.domElement); + + this.camera = new THREE.PerspectiveCamera(options.fov, initW / initH, 0.1, 10000); + this.camera.position.z = -5; + this.camera.position.y = 8; + this.camera.position.x = 0; + this.scene = new THREE.Scene(); + this.scene.background = null; + + let fog = new THREE.Fog(options.colors.background, options.length * 0.2, options.length * 500); + this.scene.fog = fog; + this.fogUniforms = { + fogColor: { value: fog.color }, + fogNear: { value: fog.near }, + fogFar: { value: fog.far } + }; + this.clock = new THREE.Clock(); + this.assets = {}; + this.disposed = false; + + this.road = new Road(this, options); + this.leftCarLights = new CarLights( + this, + options, + options.colors.leftCars, + options.movingAwaySpeed, + new THREE.Vector2(0, 1 - options.carLightsFade) + ); + this.rightCarLights = new CarLights( + this, + options, + options.colors.rightCars, + options.movingCloserSpeed, + new THREE.Vector2(1, 0 + options.carLightsFade) + ); + this.leftSticks = new LightsSticks(this, options); + + this.fovTarget = options.fov; + this.speedUpTarget = 0; + this.speedUp = 0; + this.timeOffset = 0; + + this.tick = this.tick.bind(this); + this.init = this.init.bind(this); + this.setSize = this.setSize.bind(this); + this.onMouseDown = this.onMouseDown.bind(this); + this.onMouseUp = this.onMouseUp.bind(this); + + this.onTouchStart = this.onTouchStart.bind(this); + this.onTouchEnd = this.onTouchEnd.bind(this); + this.onContextMenu = this.onContextMenu.bind(this); + + this.onWindowResize = this.onWindowResize.bind(this); + window.addEventListener('resize', this.onWindowResize); + + if (container.offsetWidth > 0 && container.offsetHeight > 0) { + this.hasValidSize = true; + } + } + + onWindowResize() { + const width = this.container.offsetWidth; + const height = this.container.offsetHeight; + + if (width <= 0 || height <= 0) { + this.hasValidSize = false; + return; + } + + this.renderer.setSize(width, height); + this.camera.aspect = width / height; + this.camera.updateProjectionMatrix(); + this.composer.setSize(width, height); + this.hasValidSize = true; + } + + initPasses() { + this.renderPass = new RenderPass(this.scene, this.camera); + this.bloomPass = new EffectPass( + this.camera, + new BloomEffect({ + luminanceThreshold: 0.2, + luminanceSmoothing: 0, + resolutionScale: 1 + }) + ); + + const smaaPass = new EffectPass( + this.camera, + new SMAAEffect({ + preset: SMAAPreset.MEDIUM, + searchImage: SMAAEffect.searchImageDataURL, + areaImage: SMAAEffect.areaImageDataURL + }) + ); + this.renderPass.renderToScreen = false; + this.bloomPass.renderToScreen = false; + smaaPass.renderToScreen = true; + this.composer.addPass(this.renderPass); + this.composer.addPass(this.bloomPass); + this.composer.addPass(smaaPass); + } + + loadAssets() { + const assets = this.assets; + return new Promise(resolve => { + const manager = new THREE.LoadingManager(resolve); + + const searchImage = new Image(); + const areaImage = new Image(); + assets.smaa = {}; + searchImage.addEventListener('load', function () { + assets.smaa.search = this; + manager.itemEnd('smaa-search'); + }); + + areaImage.addEventListener('load', function () { + assets.smaa.area = this; + manager.itemEnd('smaa-area'); + }); + manager.itemStart('smaa-search'); + manager.itemStart('smaa-area'); + + searchImage.src = SMAAEffect.searchImageDataURL; + areaImage.src = SMAAEffect.areaImageDataURL; + }); + } + + init() { + this.initPasses(); + const options = this.options; + this.road.init(); + this.leftCarLights.init(); + + this.leftCarLights.mesh.position.setX(-options.roadWidth / 2 - options.islandWidth / 2); + this.rightCarLights.init(); + this.rightCarLights.mesh.position.setX(options.roadWidth / 2 + options.islandWidth / 2); + this.leftSticks.init(); + this.leftSticks.mesh.position.setX(-(options.roadWidth + options.islandWidth / 2)); + + this.container.addEventListener('mousedown', this.onMouseDown); + this.container.addEventListener('mouseup', this.onMouseUp); + this.container.addEventListener('mouseout', this.onMouseUp); + + this.container.addEventListener('touchstart', this.onTouchStart, { passive: true }); + this.container.addEventListener('touchend', this.onTouchEnd, { passive: true }); + this.container.addEventListener('touchcancel', this.onTouchEnd, { passive: true }); + + this.container.addEventListener('contextmenu', this.onContextMenu); + + this.tick(); + } + + onMouseDown(ev) { + if (this.options.onSpeedUp) this.options.onSpeedUp(ev); + this.fovTarget = this.options.fovSpeedUp; + this.speedUpTarget = this.options.speedUp; + } + + onMouseUp(ev) { + if (this.options.onSlowDown) this.options.onSlowDown(ev); + this.fovTarget = this.options.fov; + this.speedUpTarget = 0; + } + + onTouchStart(ev) { + if (this.options.onSpeedUp) this.options.onSpeedUp(ev); + this.fovTarget = this.options.fovSpeedUp; + this.speedUpTarget = this.options.speedUp; + } + + onTouchEnd(ev) { + if (this.options.onSlowDown) this.options.onSlowDown(ev); + this.fovTarget = this.options.fov; + this.speedUpTarget = 0; + } + + onContextMenu(ev) { + ev.preventDefault(); + } + + update(delta) { + let lerpPercentage = Math.exp(-(-60 * Math.log2(1 - 0.1)) * delta); + this.speedUp += lerp(this.speedUp, this.speedUpTarget, lerpPercentage, 0.00001); + this.timeOffset += this.speedUp * delta; + + let time = this.clock.elapsedTime + this.timeOffset; + + this.rightCarLights.update(time); + this.leftCarLights.update(time); + this.leftSticks.update(time); + this.road.update(time); + + let updateCamera = false; + let fovChange = lerp(this.camera.fov, this.fovTarget, lerpPercentage); + if (fovChange !== 0) { + this.camera.fov += fovChange * delta * 6; + updateCamera = true; + } + + if (this.options.distortion.getJS) { + const distortion = this.options.distortion.getJS(0.025, time); + + this.camera.lookAt( + new THREE.Vector3( + this.camera.position.x + distortion.x, + this.camera.position.y + distortion.y, + this.camera.position.z + distortion.z + ) + ); + updateCamera = true; + } + if (updateCamera) { + this.camera.updateProjectionMatrix(); + } + } + + render(delta) { + this.composer.render(delta); + } + + dispose() { + this.disposed = true; + + if (this.scene) { + this.scene.traverse(object => { + const obj = object; + if (!obj.isMesh) return; + + if (obj.geometry) obj.geometry.dispose(); + + if (obj.material) { + if (Array.isArray(obj.material)) { + obj.material.forEach(material => material.dispose()); + } else { + obj.material.dispose(); + } + } + }); + this.scene.clear(); + } + + if (this.renderer) { + this.renderer.dispose(); + this.renderer.forceContextLoss(); + if (this.renderer.domElement && this.renderer.domElement.parentNode) { + this.renderer.domElement.parentNode.removeChild(this.renderer.domElement); + } + } + if (this.composer) { + this.composer.dispose(); + } + + window.removeEventListener('resize', this.onWindowResize); + if (this.container) { + this.container.removeEventListener('mousedown', this.onMouseDown); + this.container.removeEventListener('mouseup', this.onMouseUp); + this.container.removeEventListener('mouseout', this.onMouseUp); + + this.container.removeEventListener('touchstart', this.onTouchStart); + this.container.removeEventListener('touchend', this.onTouchEnd); + this.container.removeEventListener('touchcancel', this.onTouchEnd); + this.container.removeEventListener('contextmenu', this.onContextMenu); + } + } + + setSize(width, height, updateStyles) { + if (width <= 0 || height <= 0) { + this.hasValidSize = false; + return; + } + this.composer.setSize(width, height, updateStyles); + this.hasValidSize = true; + } + + tick() { + if (this.disposed) return; + + if (!this.hasValidSize) { + const w = this.container.offsetWidth; + const h = this.container.offsetHeight; + if (w > 0 && h > 0) { + this.renderer.setSize(w, h, false); + this.camera.aspect = w / h; + this.camera.updateProjectionMatrix(); + this.composer.setSize(w, h); + this.hasValidSize = true; + } else { + requestAnimationFrame(this.tick); + return; + } + } + + if (resizeRendererToDisplaySize(this.renderer, this.setSize)) { + const canvas = this.renderer.domElement; + if (this.hasValidSize) { + this.camera.aspect = canvas.clientWidth / canvas.clientHeight; + this.camera.updateProjectionMatrix(); + } + } + + if (this.hasValidSize) { + const delta = this.clock.getDelta(); + this.render(delta); + this.update(delta); + } + + requestAnimationFrame(this.tick); + } + } + + const distortion_uniforms = { + uDistortionX: { value: new THREE.Vector2(80, 3) }, + uDistortionY: { value: new THREE.Vector2(-40, 2.5) } + }; + + const distortion_vertex = ` + #define PI 3.14159265358979 + uniform vec2 uDistortionX; + uniform vec2 uDistortionY; + float nsin(float val){ + return sin(val) * 0.5 + 0.5; + } + vec3 getDistortion(float progress){ + progress = clamp(progress, 0., 1.); + float xAmp = uDistortionX.r; + float xFreq = uDistortionX.g; + float yAmp = uDistortionY.r; + float yFreq = uDistortionY.g; + return vec3( + xAmp * nsin(progress * PI * xFreq - PI / 2.), + yAmp * nsin(progress * PI * yFreq - PI / 2.), + 0. + ); + } + `; + + const random = base => { + if (Array.isArray(base)) return Math.random() * (base[1] - base[0]) + base[0]; + return Math.random() * base; + }; + + const pickRandom = arr => { + if (Array.isArray(arr)) return arr[Math.floor(Math.random() * arr.length)]; + return arr; + }; + + function lerp(current, target, speed = 0.1, limit = 0.001) { + let change = (target - current) * speed; + if (Math.abs(change) < limit) { + change = target - current; + } + return change; + } + + class CarLights { + constructor(webgl, options, colors, speed, fade) { + this.webgl = webgl; + this.options = options; + this.colors = colors; + this.speed = speed; + this.fade = fade; + } + + init() { + const options = this.options; + let curve = new THREE.LineCurve3(new THREE.Vector3(0, 0, 0), new THREE.Vector3(0, 0, -1)); + let geometry = new THREE.TubeGeometry(curve, 40, 1, 8, false); + + let instanced = new THREE.InstancedBufferGeometry().copy(geometry); + instanced.instanceCount = options.lightPairsPerRoadWay * 2; + + let laneWidth = options.roadWidth / options.lanesPerRoad; + + let aOffset = []; + let aMetrics = []; + let aColor = []; + + let colors = this.colors; + if (Array.isArray(colors)) { + colors = colors.map(c => new THREE.Color(c)); + } else { + colors = new THREE.Color(colors); + } + + for (let i = 0; i < options.lightPairsPerRoadWay; i++) { + let radius = random(options.carLightsRadius); + let length = random(options.carLightsLength); + let speed = random(this.speed); + + let carLane = i % options.lanesPerRoad; + let laneX = carLane * laneWidth - options.roadWidth / 2 + laneWidth / 2; + + let carWidth = random(options.carWidthPercentage) * laneWidth; + let carShiftX = random(options.carShiftX) * laneWidth; + laneX += carShiftX; + + let offsetY = random(options.carFloorSeparation) + radius * 1.3; + + let offsetZ = -random(options.length); + + aOffset.push(laneX - carWidth / 2); + aOffset.push(offsetY); + aOffset.push(offsetZ); + + aOffset.push(laneX + carWidth / 2); + aOffset.push(offsetY); + aOffset.push(offsetZ); + + aMetrics.push(radius); + aMetrics.push(length); + aMetrics.push(speed); + + aMetrics.push(radius); + aMetrics.push(length); + aMetrics.push(speed); + + let color = pickRandom(colors); + aColor.push(color.r); + aColor.push(color.g); + aColor.push(color.b); + + aColor.push(color.r); + aColor.push(color.g); + aColor.push(color.b); + } + + instanced.setAttribute('aOffset', new THREE.InstancedBufferAttribute(new Float32Array(aOffset), 3, false)); + instanced.setAttribute('aMetrics', new THREE.InstancedBufferAttribute(new Float32Array(aMetrics), 3, false)); + instanced.setAttribute('aColor', new THREE.InstancedBufferAttribute(new Float32Array(aColor), 3, false)); + + let material = new THREE.ShaderMaterial({ + fragmentShader: carLightsFragment, + vertexShader: carLightsVertex, + transparent: true, + uniforms: Object.assign( + { + uTime: { value: 0 }, + uTravelLength: { value: options.length }, + uFade: { value: this.fade } + }, + this.webgl.fogUniforms, + options.distortion.uniforms + ) + }); + + material.onBeforeCompile = shader => { + shader.vertexShader = shader.vertexShader.replace( + '#include ', + options.distortion.getDistortion + ); + }; + + let mesh = new THREE.Mesh(instanced, material); + mesh.frustumCulled = false; + this.webgl.scene.add(mesh); + this.mesh = mesh; + } + + update(time) { + this.mesh.material.uniforms.uTime.value = time; + } + } + + const carLightsFragment = ` + #define USE_FOG; + ${THREE.ShaderChunk['fog_pars_fragment']} + varying vec3 vColor; + varying vec2 vUv; + uniform vec2 uFade; + void main() { + vec3 color = vec3(vColor); + float alpha = smoothstep(uFade.x, uFade.y, vUv.x); + gl_FragColor = vec4(color, alpha); + if (gl_FragColor.a < 0.0001) discard; + ${THREE.ShaderChunk['fog_fragment']} + } + `; + + const carLightsVertex = ` + #define USE_FOG; + ${THREE.ShaderChunk['fog_pars_vertex']} + attribute vec3 aOffset; + attribute vec3 aMetrics; + attribute vec3 aColor; + uniform float uTravelLength; + uniform float uTime; + varying vec2 vUv; + varying vec3 vColor; + #include + void main() { + vec3 transformed = position.xyz; + float radius = aMetrics.r; + float myLength = aMetrics.g; + float speed = aMetrics.b; + + transformed.xy *= radius; + transformed.z *= myLength; + + transformed.z += myLength - mod(uTime * speed + aOffset.z, uTravelLength); + transformed.xy += aOffset.xy; + + float progress = abs(transformed.z / uTravelLength); + transformed.xyz += getDistortion(progress); + + vec4 mvPosition = modelViewMatrix * vec4(transformed, 1.); + gl_Position = projectionMatrix * mvPosition; + vUv = uv; + vColor = aColor; + ${THREE.ShaderChunk['fog_vertex']} + } + `; + + class LightsSticks { + constructor(webgl, options) { + this.webgl = webgl; + this.options = options; + } + + init() { + const options = this.options; + const geometry = new THREE.PlaneGeometry(1, 1); + let instanced = new THREE.InstancedBufferGeometry().copy(geometry); + let totalSticks = options.totalSideLightSticks; + instanced.instanceCount = totalSticks; + + let stickoffset = options.length / (totalSticks - 1); + const aOffset = []; + const aColor = []; + const aMetrics = []; + + let colors = options.colors.sticks; + if (Array.isArray(colors)) { + colors = colors.map(c => new THREE.Color(c)); + } else { + colors = new THREE.Color(colors); + } + + for (let i = 0; i < totalSticks; i++) { + let width = random(options.lightStickWidth); + let height = random(options.lightStickHeight); + aOffset.push((i - 1) * stickoffset * 2 + stickoffset * Math.random()); + + let color = pickRandom(colors); + aColor.push(color.r); + aColor.push(color.g); + aColor.push(color.b); + + aMetrics.push(width); + aMetrics.push(height); + } + + instanced.setAttribute('aOffset', new THREE.InstancedBufferAttribute(new Float32Array(aOffset), 1, false)); + instanced.setAttribute('aColor', new THREE.InstancedBufferAttribute(new Float32Array(aColor), 3, false)); + instanced.setAttribute('aMetrics', new THREE.InstancedBufferAttribute(new Float32Array(aMetrics), 2, false)); + + const material = new THREE.ShaderMaterial({ + fragmentShader: sideSticksFragment, + vertexShader: sideSticksVertex, + side: THREE.DoubleSide, + uniforms: Object.assign( + { + uTravelLength: { value: options.length }, + uTime: { value: 0 } + }, + this.webgl.fogUniforms, + options.distortion.uniforms + ) + }); + + material.onBeforeCompile = shader => { + shader.vertexShader = shader.vertexShader.replace( + '#include ', + options.distortion.getDistortion + ); + }; + + const mesh = new THREE.Mesh(instanced, material); + mesh.frustumCulled = false; + this.webgl.scene.add(mesh); + this.mesh = mesh; + } + + update(time) { + this.mesh.material.uniforms.uTime.value = time; + } + } + + const sideSticksVertex = ` + #define USE_FOG; + ${THREE.ShaderChunk['fog_pars_vertex']} + attribute float aOffset; + attribute vec3 aColor; + attribute vec2 aMetrics; + uniform float uTravelLength; + uniform float uTime; + varying vec3 vColor; + mat4 rotationY( in float angle ) { + return mat4( cos(angle), 0, sin(angle), 0, + 0, 1.0, 0, 0, + -sin(angle), 0, cos(angle), 0, + 0, 0, 0, 1); + } + #include + void main(){ + vec3 transformed = position.xyz; + float width = aMetrics.x; + float height = aMetrics.y; + + transformed.xy *= vec2(width, height); + float time = mod(uTime * 60. * 2. + aOffset, uTravelLength); + + transformed = (rotationY(3.14/2.) * vec4(transformed,1.)).xyz; + + transformed.z += - uTravelLength + time; + + float progress = abs(transformed.z / uTravelLength); + transformed.xyz += getDistortion(progress); + + transformed.y += height / 2.; + transformed.x += -width / 2.; + vec4 mvPosition = modelViewMatrix * vec4(transformed, 1.); + gl_Position = projectionMatrix * mvPosition; + vColor = aColor; + ${THREE.ShaderChunk['fog_vertex']} + } + `; + + const sideSticksFragment = ` + #define USE_FOG; + ${THREE.ShaderChunk['fog_pars_fragment']} + varying vec3 vColor; + void main(){ + vec3 color = vec3(vColor); + gl_FragColor = vec4(color,1.); + ${THREE.ShaderChunk['fog_fragment']} + } + `; + + class Road { + constructor(webgl, options) { + this.webgl = webgl; + this.options = options; + this.uTime = { value: 0 }; + } + + createPlane(side, width, isRoad) { + const options = this.options; + let segments = 100; + const geometry = new THREE.PlaneGeometry( + isRoad ? options.roadWidth : options.islandWidth, + options.length, + 20, + segments + ); + let uniforms = { + uTravelLength: { value: options.length }, + uColor: { value: new THREE.Color(isRoad ? options.colors.roadColor : options.colors.islandColor) }, + uTime: this.uTime + }; + + if (isRoad) { + uniforms = Object.assign(uniforms, { + uLanes: { value: options.lanesPerRoad }, + uBrokenLinesColor: { value: new THREE.Color(options.colors.brokenLines) }, + uShoulderLinesColor: { value: new THREE.Color(options.colors.shoulderLines) }, + uShoulderLinesWidthPercentage: { value: options.shoulderLinesWidthPercentage }, + uBrokenLinesLengthPercentage: { value: options.brokenLinesLengthPercentage }, + uBrokenLinesWidthPercentage: { value: options.brokenLinesWidthPercentage } + }); + } + + const material = new THREE.ShaderMaterial({ + fragmentShader: isRoad ? roadFragment : islandFragment, + vertexShader: roadVertex, + side: THREE.DoubleSide, + uniforms: Object.assign(uniforms, this.webgl.fogUniforms, options.distortion.uniforms) + }); + + material.onBeforeCompile = shader => { + shader.vertexShader = shader.vertexShader.replace( + '#include ', + options.distortion.getDistortion + ); + }; + + const mesh = new THREE.Mesh(geometry, material); + mesh.rotation.x = -Math.PI / 2; + mesh.position.z = -options.length / 2; + mesh.position.x += (this.options.islandWidth / 2 + options.roadWidth / 2) * side; + this.webgl.scene.add(mesh); + + return mesh; + } + + init() { + this.leftRoadWay = this.createPlane(-1, this.options.roadWidth, true); + this.rightRoadWay = this.createPlane(1, this.options.roadWidth, true); + this.island = this.createPlane(0, this.options.islandWidth, false); + } + + update(time) { + this.uTime.value = time; + } + } + + const roadBaseFragment = ` + #define USE_FOG; + varying vec2 vUv; + uniform vec3 uColor; + uniform float uTime; + #include + ${THREE.ShaderChunk['fog_pars_fragment']} + void main() { + vec2 uv = vUv; + vec3 color = vec3(uColor); + #include + gl_FragColor = vec4(color, 1.); + ${THREE.ShaderChunk['fog_fragment']} + } + `; + + const islandFragment = roadBaseFragment + .replace('#include ', '') + .replace('#include ', ''); + + const roadMarkings_vars = ` + uniform float uLanes; + uniform vec3 uBrokenLinesColor; + uniform vec3 uShoulderLinesColor; + uniform float uShoulderLinesWidthPercentage; + uniform float uBrokenLinesWidthPercentage; + uniform float uBrokenLinesLengthPercentage; + highp float random(vec2 co) { + highp float a = 12.9898; + highp float b = 78.233; + highp float c = 43758.5453; + highp float dt = dot(co.xy, vec2(a, b)); + highp float sn = mod(dt, 3.14); + return fract(sin(sn) * c); + } + `; + + const roadMarkings_fragment = ` + uv.y = mod(uv.y + uTime * 0.05, 1.); + float laneWidth = 1.0 / uLanes; + float brokenLineWidth = laneWidth * uBrokenLinesWidthPercentage; + float laneEmptySpace = 1. - uBrokenLinesLengthPercentage; + + float brokenLines = step(1.0 - brokenLineWidth, fract(uv.x * 2.0)) * step(laneEmptySpace, fract(uv.y * 10.0)); + float sideLines = step(1.0 - brokenLineWidth, fract((uv.x - laneWidth * (uLanes - 1.0)) * 2.0)) + step(brokenLineWidth, uv.x); + + brokenLines = mix(brokenLines, sideLines, uv.x); + `; + + const roadFragment = roadBaseFragment + .replace('#include ', roadMarkings_fragment) + .replace('#include ', roadMarkings_vars); + + const roadVertex = ` + #define USE_FOG; + uniform float uTime; + ${THREE.ShaderChunk['fog_pars_vertex']} + uniform float uTravelLength; + varying vec2 vUv; + #include + void main() { + vec3 transformed = position.xyz; + vec3 distortion = getDistortion((transformed.y + uTravelLength / 2.) / uTravelLength); + transformed.x += distortion.x; + transformed.z += distortion.y; + transformed.y += -1. * distortion.z; + + vec4 mvPosition = modelViewMatrix * vec4(transformed, 1.); + gl_Position = projectionMatrix * mvPosition; + vUv = uv; + ${THREE.ShaderChunk['fog_vertex']} + } + `; + + function resizeRendererToDisplaySize(renderer, setSize) { + const canvas = renderer.domElement; + const width = canvas.clientWidth; + const height = canvas.clientHeight; + if (width <= 0 || height <= 0) return false; + const needResize = canvas.width !== width || canvas.height !== height; + if (needResize) { + setSize(width, height, false); + } + return needResize; + } + + const container = hyperspeed.current; + if (!container) return; + + const options = { + ...DEFAULT_EFFECT_OPTIONS, + ...effectOptions, + colors: { ...DEFAULT_EFFECT_OPTIONS.colors, ...effectOptions.colors } + }; + options.distortion = distortions[options.distortion]; + + const myApp = new App(container, options); + appRef.current = myApp; + let cancelled = false; + myApp.loadAssets().then(() => { + if (!cancelled && !myApp.disposed) { + myApp.init(); + } + }); + + return () => { + cancelled = true; + if (appRef.current) { + appRef.current.dispose(); + appRef.current = null; + } + }; + }, [effectOptions]); + + return
; +}; + +export default Hyperspeed; diff --git a/apps/web-dashboard/src/components/MagicBento/MagicBento.css b/apps/web-dashboard/src/components/MagicBento/MagicBento.css new file mode 100644 index 000000000..88a7b8b85 --- /dev/null +++ b/apps/web-dashboard/src/components/MagicBento/MagicBento.css @@ -0,0 +1,297 @@ +.bento-section { + --hue: 27; + --sat: 69%; + --white: hsl(0, 0%, 100%); + --purple-primary: rgba(0, 245, 212, 1); + --purple-glow: rgba(0, 245, 212, 0.2); + --purple-border: rgba(0, 245, 212, 0.8); + --border-color: #2F293A; + --background-dark: #120F17; +} + +.card-grid { + display: grid; + gap: 0.75em; + padding: 1em; + max-width: 72em; + font-size: clamp(1rem, 0.9rem + 0.5vw, 1.5rem); +} + +.magic-bento-card { + display: flex; + flex-direction: column; + justify-content: space-between; + position: relative; + min-height: 230px; + width: 100%; + max-width: 100%; + padding: 1.4em; + border-radius: 16px; + border: 1px solid rgba(255, 255, 255, 0.08); + background: rgba(255, 255, 255, 0.02); + backdrop-filter: blur(12px); + -webkit-backdrop-filter: blur(12px); + font-weight: 300; + overflow: visible; + transition: all 0.4s cubic-bezier(0.16, 1, 0.3, 1); + + --glow-x: 50%; + --glow-y: 50%; + --glow-intensity: 0; + --glow-radius: 200px; +} + +.magic-bento-card:hover { + transform: translateY(-4px); + border-color: rgba(0, 245, 212, 0.3); + background: rgba(0, 245, 212, 0.04); + box-shadow: 0 12px 32px rgba(0, 0, 0, 0.3), 0 0 20px rgba(0, 245, 212, 0.08); +} + +.magic-bento-card__header, +.magic-bento-card__content { + display: flex; + position: relative; + color: var(--white); +} + +.magic-bento-card__header { + gap: 0.75em; + justify-content: space-between; + align-items: center; +} + +.magic-bento-card__content { + flex-direction: column; +} + +.magic-bento-card__label { + font-size: 10px; + font-weight: 600; + letter-spacing: 0.1em; + text-transform: uppercase; + color: rgba(0, 245, 212, 0.6); + padding: 0.2em 0.6em; + border-radius: 4px; + background: rgba(0, 245, 212, 0.06); + border: 1px solid rgba(0, 245, 212, 0.1); + transition: all 0.4s ease; + transform: translateY(0); + opacity: 0.7; +} + +.magic-bento-card:hover .magic-bento-card__label { + color: rgba(0, 245, 212, 0.9); + background: rgba(0, 245, 212, 0.1); + border-color: rgba(0, 245, 212, 0.25); + opacity: 1; + transform: translateY(-1px); +} + +.magic-bento-card__title, +.magic-bento-card__description { + --clamp-title: 1; + --clamp-desc: 2; +} + +.magic-bento-card__title { + font-weight: 500; + font-size: 15px; + margin: 0 0 0.35em; + color: #f0f0f0; + letter-spacing: -0.01em; + transition: all 0.35s ease 0.05s; + transform: translateY(0); +} + +.magic-bento-card:hover .magic-bento-card__title { + color: #ffffff; + transform: translateY(-2px); +} + +.magic-bento-card__description { + font-size: 12px; + line-height: 1.45; + color: rgba(255, 255, 255, 0.45); + transition: all 0.4s ease 0.1s; + transform: translateY(0); +} + +.magic-bento-card:hover .magic-bento-card__description { + color: rgba(255, 255, 255, 0.7); + transform: translateY(-2px); +} + +.magic-bento-card--text-autohide .magic-bento-card__title, +.magic-bento-card--text-autohide .magic-bento-card__description { + display: -webkit-box; + -webkit-box-orient: vertical; + overflow: hidden; + text-overflow: ellipsis; +} + +.magic-bento-card--text-autohide .magic-bento-card__title { + -webkit-line-clamp: var(--clamp-title); + line-clamp: var(--clamp-title); +} + +.magic-bento-card--text-autohide .magic-bento-card__description { + -webkit-line-clamp: var(--clamp-desc); + line-clamp: var(--clamp-desc); +} + +@media (max-width: 599px) { + .card-grid { + grid-template-columns: 1fr; + width: 92%; + margin: 0 auto; + padding: 0.5em; + } + + .magic-bento-card { + width: 100%; + min-height: 200px; + } +} + +@media (min-width: 600px) { + .card-grid { + grid-template-columns: repeat(2, 1fr); + } +} + +@media (min-width: 1024px) { + .card-grid { + grid-template-columns: repeat(4, 1fr); + } + + .magic-bento-card:nth-child(3) { + grid-column: span 2; + grid-row: span 2; + } + + .magic-bento-card:nth-child(4) { + grid-column: 1 / span 2; + grid-row: 2 / span 2; + } + + .magic-bento-card:nth-child(6) { + grid-column: 4; + grid-row: 3; + } +} + +.magic-bento-card--border-glow::after { + content: ''; + position: absolute; + inset: 0; + padding: 6px; + background: radial-gradient( + var(--glow-radius) circle at var(--glow-x) var(--glow-y), + rgba(0, 245, 212, calc(var(--glow-intensity) * 0.8)) 0%, + rgba(0, 245, 212, calc(var(--glow-intensity) * 0.4)) 30%, + transparent 60% + ); + border-radius: inherit; + -webkit-mask: + linear-gradient(#fff 0 0) content-box, + linear-gradient(#fff 0 0); + -webkit-mask-composite: xor; + mask: + linear-gradient(#fff 0 0) content-box, + linear-gradient(#fff 0 0); + mask-composite: exclude; + pointer-events: none; + opacity: 1; + transition: opacity 0.3s ease; + z-index: 1; +} + +.magic-bento-card--border-glow:hover::after { + opacity: 1; +} + +.magic-bento-card--border-glow:hover { + box-shadow: + 0 4px 20px rgba(0, 245, 212, 0.4), + 0 0 30px var(--purple-glow); +} + +.particle-container { + position: relative; + overflow: hidden; +} + +.particle::before { + content: ''; + position: absolute; + top: -2px; + left: -2px; + right: -2px; + bottom: -2px; + background: rgba(0, 245, 212, 0.2); + border-radius: 50%; + z-index: -1; +} + +.particle-container:hover { + box-shadow: + 0 4px 20px rgba(0, 245, 212, 0.2), + 0 0 30px var(--purple-glow); +} + +.global-spotlight { + mix-blend-mode: screen; + will-change: transform, opacity; + z-index: 200 !important; + pointer-events: none; +} + +.bento-section { + position: relative; + user-select: none; +} + +.magic-bento-card__visual { + display: flex; + align-items: center; + justify-content: center; + flex: 1; + width: 100%; + height: 100%; + min-height: 110px; + overflow: hidden; + position: relative; + z-index: 2; + margin: 0.35em 0; + opacity: 0.85; + transition: opacity 0.4s ease; +} + +.magic-bento-card:hover .magic-bento-card__visual { + opacity: 1; +} + +/* Default mockup scaling for 1x1 cards to fit comfortably */ +.magic-bento-card__visual > * { + transform: scale(0.88); + transform-origin: center; + transition: transform 0.4s cubic-bezier(0.16, 1, 0.3, 1); +} + +.magic-bento-card:hover .magic-bento-card__visual > * { + transform: scale(0.94); +} + +/* Larger mockup scale for 2x2 double-width cards on desktop */ +@media (min-width: 1024px) { + .magic-bento-card:nth-child(3) .magic-bento-card__visual > *, + .magic-bento-card:nth-child(4) .magic-bento-card__visual > * { + transform: scale(1.1); + } + + .magic-bento-card:nth-child(3):hover .magic-bento-card__visual > *, + .magic-bento-card:nth-child(4):hover .magic-bento-card__visual > * { + transform: scale(1.15); + } +} diff --git a/apps/web-dashboard/src/components/MagicBento/MagicBento.jsx b/apps/web-dashboard/src/components/MagicBento/MagicBento.jsx new file mode 100644 index 000000000..2d35c8f9f --- /dev/null +++ b/apps/web-dashboard/src/components/MagicBento/MagicBento.jsx @@ -0,0 +1,556 @@ +import { useRef, useEffect, useCallback, useState } from 'react'; +import { gsap } from 'gsap'; +import './MagicBento.css'; +import BorderGlow from '../BorderGlow/BorderGlow'; + +const DEFAULT_PARTICLE_COUNT = 12; +const DEFAULT_SPOTLIGHT_RADIUS = 300; +const DEFAULT_GLOW_COLOR = '0, 245, 212'; +const MOBILE_BREAKPOINT = 768; + +const cardData = [ + { + color: '#120F17', + title: 'User Authentication', + description: 'Pre-configured secure login/signup flows, session handling with JWTs, and third-party login providers with zero setup.', + label: 'IDENTITY' + }, + { + color: '#120F17', + title: 'JSON Document Database', + description: 'Direct database mutations and queries from your client app. Automated validation schemas protect integrity at high speed.', + label: 'DATABASE' + }, + { + color: '#120F17', + title: 'Secure File Storage', + description: 'Upload, manage, and deliver media assets like avatars and documents. Integrated with bucket systems and global delivery networks.', + label: 'STORAGE' + }, + { + color: '#120F17', + title: 'Transactional Mailer', + description: 'Pre-wired email dispatchers for user verification, password recovery, and system notifications with customizable templates.', + label: 'COMMUNICATION' + }, + { + color: '#120F17', + title: 'Row-Level Security', + description: 'Fine-grained read/write security with RLS. Restrict access based on document ownership and user authentication tokens.', + label: 'SECURITY' + }, + { + color: '#120F17', + title: 'Realtime Broadcast', + description: 'Listen to database mutations instantly or broadcast custom events across clients using low-latency WebSocket connections.', + label: 'REALTIME' + } +]; + + +const createParticleElement = (x, y, color = DEFAULT_GLOW_COLOR) => { + const el = document.createElement('div'); + el.className = 'particle'; + el.style.cssText = ` + position: absolute; + width: 4px; + height: 4px; + border-radius: 50%; + background: rgba(${color}, 1); + box-shadow: 0 0 6px rgba(${color}, 0.6); + pointer-events: none; + z-index: 100; + left: ${x}px; + top: ${y}px; + `; + return el; +}; + +const calculateSpotlightValues = radius => ({ + proximity: radius * 0.5, + fadeDistance: radius * 0.75 +}); + +const updateCardGlowProperties = (card, mouseX, mouseY, glow, radius) => { + const rect = card.getBoundingClientRect(); + const relativeX = ((mouseX - rect.left) / rect.width) * 100; + const relativeY = ((mouseY - rect.top) / rect.height) * 100; + + card.style.setProperty('--glow-x', `${relativeX}%`); + card.style.setProperty('--glow-y', `${relativeY}%`); + card.style.setProperty('--glow-intensity', glow.toString()); + card.style.setProperty('--glow-radius', `${radius}px`); +}; + +const ParticleCard = ({ + children, + className = '', + disableAnimations = false, + style, + particleCount = DEFAULT_PARTICLE_COUNT, + glowColor = DEFAULT_GLOW_COLOR, + enableTilt = true, + clickEffect = false, + enableMagnetism = false +}) => { + const cardRef = useRef(null); + const particlesRef = useRef([]); + const timeoutsRef = useRef([]); + const isHoveredRef = useRef(false); + const memoizedParticles = useRef([]); + const particlesInitialized = useRef(false); + const magnetismAnimationRef = useRef(null); + + const initializeParticles = useCallback(() => { + if (particlesInitialized.current || !cardRef.current) return; + + const { width, height } = cardRef.current.getBoundingClientRect(); + memoizedParticles.current = Array.from({ length: particleCount }, () => + createParticleElement(Math.random() * width, Math.random() * height, glowColor) + ); + particlesInitialized.current = true; + }, [particleCount, glowColor]); + + const clearAllParticles = useCallback(() => { + timeoutsRef.current.forEach(clearTimeout); + timeoutsRef.current = []; + magnetismAnimationRef.current?.kill(); + + particlesRef.current.forEach(particle => { + gsap.to(particle, { + scale: 0, + opacity: 0, + duration: 0.3, + ease: 'back.in(1.7)', + onComplete: () => { + particle.parentNode?.removeChild(particle); + } + }); + }); + particlesRef.current = []; + }, []); + + const animateParticles = useCallback(() => { + if (!cardRef.current || !isHoveredRef.current) return; + + if (!particlesInitialized.current) { + initializeParticles(); + } + + memoizedParticles.current.forEach((particle, index) => { + const timeoutId = setTimeout(() => { + if (!isHoveredRef.current || !cardRef.current) return; + + const clone = particle.cloneNode(true); + cardRef.current.appendChild(clone); + particlesRef.current.push(clone); + + gsap.fromTo(clone, { scale: 0, opacity: 0 }, { scale: 1, opacity: 1, duration: 0.3, ease: 'back.out(1.7)' }); + + gsap.to(clone, { + x: (Math.random() - 0.5) * 100, + y: (Math.random() - 0.5) * 100, + rotation: Math.random() * 360, + duration: 2 + Math.random() * 2, + ease: 'none', + repeat: -1, + yoyo: true + }); + + gsap.to(clone, { + opacity: 0.3, + duration: 1.5, + ease: 'power2.inOut', + repeat: -1, + yoyo: true + }); + }, index * 100); + + timeoutsRef.current.push(timeoutId); + }); + }, [initializeParticles]); + + useEffect(() => { + if (disableAnimations || !cardRef.current) return; + + const element = cardRef.current; + + const handleMouseEnter = () => { + isHoveredRef.current = true; + animateParticles(); + + if (enableTilt) { + gsap.to(element, { + rotateX: 5, + rotateY: 5, + duration: 0.3, + ease: 'power2.out', + transformPerspective: 1000 + }); + } + }; + + const handleMouseLeave = () => { + isHoveredRef.current = false; + clearAllParticles(); + + if (enableTilt) { + gsap.to(element, { + rotateX: 0, + rotateY: 0, + duration: 0.3, + ease: 'power2.out' + }); + } + + if (enableMagnetism) { + gsap.to(element, { + x: 0, + y: 0, + duration: 0.3, + ease: 'power2.out' + }); + } + }; + + const handleMouseMove = e => { + if (!enableTilt && !enableMagnetism) return; + + const rect = element.getBoundingClientRect(); + const x = e.clientX - rect.left; + const y = e.clientY - rect.top; + const centerX = rect.width / 2; + const centerY = rect.height / 2; + + if (enableTilt) { + const rotateX = ((y - centerY) / centerY) * -10; + const rotateY = ((x - centerX) / centerX) * 10; + + gsap.to(element, { + rotateX, + rotateY, + duration: 0.1, + ease: 'power2.out', + transformPerspective: 1000 + }); + } + + if (enableMagnetism) { + const magnetX = (x - centerX) * 0.05; + const magnetY = (y - centerY) * 0.05; + + magnetismAnimationRef.current = gsap.to(element, { + x: magnetX, + y: magnetY, + duration: 0.3, + ease: 'power2.out' + }); + } + }; + + const handleClick = e => { + if (!clickEffect) return; + + const rect = element.getBoundingClientRect(); + const x = e.clientX - rect.left; + const y = e.clientY - rect.top; + + const maxDistance = Math.max( + Math.hypot(x, y), + Math.hypot(x - rect.width, y), + Math.hypot(x, y - rect.height), + Math.hypot(x - rect.width, y - rect.height) + ); + + const ripple = document.createElement('div'); + ripple.style.cssText = ` + position: absolute; + width: ${maxDistance * 2}px; + height: ${maxDistance * 2}px; + border-radius: 50%; + background: radial-gradient(circle, rgba(${glowColor}, 0.4) 0%, rgba(${glowColor}, 0.2) 30%, transparent 70%); + left: ${x - maxDistance}px; + top: ${y - maxDistance}px; + pointer-events: none; + z-index: 1000; + `; + + element.appendChild(ripple); + + gsap.fromTo( + ripple, + { + scale: 0, + opacity: 1 + }, + { + scale: 1, + opacity: 0, + duration: 0.8, + ease: 'power2.out', + onComplete: () => ripple.remove() + } + ); + }; + + element.addEventListener('mouseenter', handleMouseEnter); + element.addEventListener('mouseleave', handleMouseLeave); + element.addEventListener('mousemove', handleMouseMove); + element.addEventListener('click', handleClick); + + return () => { + isHoveredRef.current = false; + element.removeEventListener('mouseenter', handleMouseEnter); + element.removeEventListener('mouseleave', handleMouseLeave); + element.removeEventListener('mousemove', handleMouseMove); + element.removeEventListener('click', handleClick); + clearAllParticles(); + }; + }, [animateParticles, clearAllParticles, disableAnimations, enableTilt, enableMagnetism, clickEffect, glowColor]); + + return ( +
+ {children} +
+ ); +}; + +const GlobalSpotlight = ({ + gridRef, + disableAnimations = false, + enabled = true, + spotlightRadius = DEFAULT_SPOTLIGHT_RADIUS, + glowColor = DEFAULT_GLOW_COLOR +}) => { + const spotlightRef = useRef(null); + const isInsideSection = useRef(false); + + useEffect(() => { + if (disableAnimations || !gridRef?.current || !enabled) return; + + const spotlight = document.createElement('div'); + spotlight.className = 'global-spotlight'; + spotlight.style.cssText = ` + position: fixed; + width: 800px; + height: 800px; + border-radius: 50%; + pointer-events: none; + background: radial-gradient(circle, + rgba(${glowColor}, 0.15) 0%, + rgba(${glowColor}, 0.08) 15%, + rgba(${glowColor}, 0.04) 25%, + rgba(${glowColor}, 0.02) 40%, + rgba(${glowColor}, 0.01) 65%, + transparent 70% + ); + z-index: 200; + opacity: 0; + transform: translate(-50%, -50%); + mix-blend-mode: screen; + `; + document.body.appendChild(spotlight); + spotlightRef.current = spotlight; + + const handleMouseMove = e => { + if (!spotlightRef.current || !gridRef.current) return; + + const section = gridRef.current.closest('.bento-section'); + const rect = section?.getBoundingClientRect(); + const mouseInside = + rect && e.clientX >= rect.left && e.clientX <= rect.right && e.clientY >= rect.top && e.clientY <= rect.bottom; + + isInsideSection.current = mouseInside || false; + const cards = gridRef.current.querySelectorAll('.magic-bento-card'); + + if (!mouseInside) { + gsap.to(spotlightRef.current, { + opacity: 0, + duration: 0.3, + ease: 'power2.out' + }); + cards.forEach(card => { + card.style.setProperty('--glow-intensity', '0'); + }); + return; + } + + const { proximity, fadeDistance } = calculateSpotlightValues(spotlightRadius); + let minDistance = Infinity; + + cards.forEach(card => { + const cardElement = card; + const cardRect = cardElement.getBoundingClientRect(); + const centerX = cardRect.left + cardRect.width / 2; + const centerY = cardRect.top + cardRect.height / 2; + const distance = + Math.hypot(e.clientX - centerX, e.clientY - centerY) - Math.max(cardRect.width, cardRect.height) / 2; + const effectiveDistance = Math.max(0, distance); + + minDistance = Math.min(minDistance, effectiveDistance); + + let glowIntensity = 0; + if (effectiveDistance <= proximity) { + glowIntensity = 1; + } else if (effectiveDistance <= fadeDistance) { + glowIntensity = (fadeDistance - effectiveDistance) / (fadeDistance - proximity); + } + + updateCardGlowProperties(cardElement, e.clientX, e.clientY, glowIntensity, spotlightRadius); + }); + + gsap.to(spotlightRef.current, { + left: e.clientX, + top: e.clientY, + duration: 0.1, + ease: 'power2.out' + }); + + const targetOpacity = + minDistance <= proximity + ? 0.8 + : minDistance <= fadeDistance + ? ((fadeDistance - minDistance) / (fadeDistance - proximity)) * 0.8 + : 0; + + gsap.to(spotlightRef.current, { + opacity: targetOpacity, + duration: targetOpacity > 0 ? 0.2 : 0.5, + ease: 'power2.out' + }); + }; + + const handleMouseLeave = () => { + isInsideSection.current = false; + gridRef.current?.querySelectorAll('.magic-bento-card').forEach(card => { + card.style.setProperty('--glow-intensity', '0'); + }); + if (spotlightRef.current) { + gsap.to(spotlightRef.current, { + opacity: 0, + duration: 0.3, + ease: 'power2.out' + }); + } + }; + + document.addEventListener('mousemove', handleMouseMove); + document.addEventListener('mouseleave', handleMouseLeave); + + return () => { + document.removeEventListener('mousemove', handleMouseMove); + document.removeEventListener('mouseleave', handleMouseLeave); + spotlightRef.current?.parentNode?.removeChild(spotlightRef.current); + }; + }, [gridRef, disableAnimations, enabled, spotlightRadius, glowColor]); + + return null; +}; + +const BentoCardGrid = ({ children, gridRef }) => ( +
+ {children} +
+); + +const useMobileDetection = () => { + const [isMobile, setIsMobile] = useState(false); + + useEffect(() => { + const checkMobile = () => setIsMobile(window.innerWidth <= MOBILE_BREAKPOINT); + + checkMobile(); + window.addEventListener('resize', checkMobile); + + return () => window.removeEventListener('resize', checkMobile); + }, []); + + return isMobile; +}; + +const MagicBento = ({ + cards, + textAutoHide = true, + enableSpotlight = true, + disableAnimations = false, + spotlightRadius = DEFAULT_SPOTLIGHT_RADIUS, + particleCount = DEFAULT_PARTICLE_COUNT, + enableTilt = false, + glowColor = DEFAULT_GLOW_COLOR, + clickEffect = true, + enableMagnetism = true +}) => { + const gridRef = useRef(null); + const isMobile = useMobileDetection(); + const shouldDisableAnimations = disableAnimations || isMobile; + + return ( + <> + {enableSpotlight && ( + + )} + + + {(cards || cardData).map((card, index) => { + const title = card.title; + const description = card.description || card.desc; + const label = card.label || card.badge; + const visual = card.visual; + + const baseClassName = `magic-bento-card ${textAutoHide ? 'magic-bento-card--text-autohide' : ''}`; + + return ( + + +
+
{label}
+
+ {visual && ( +
+ {visual} +
+ )} +
+

{title}

+

{description}

+
+
+
+ ); + })} +
+ + ); +}; + +export default MagicBento; diff --git a/apps/web-dashboard/src/index.css b/apps/web-dashboard/src/index.css index 26aa88a01..18f846901 100644 --- a/apps/web-dashboard/src/index.css +++ b/apps/web-dashboard/src/index.css @@ -812,11 +812,11 @@ select option { .auth-form-card { width: 100%; - padding: 0; - border: none; - border-radius: 0; - background: transparent; - box-shadow: none; + padding: 2rem; + border: 1px solid rgba(255,255,255,0.06); + border-radius: 16px; + background: rgba(255,255,255,0.02); + box-shadow: 0 24px 48px -12px rgba(0,0,0,0.4); } .auth-form-card__brand { diff --git a/apps/web-dashboard/src/pages/LandingPage/index.jsx b/apps/web-dashboard/src/pages/LandingPage/index.jsx index 55094e8f9..4806cffbb 100644 --- a/apps/web-dashboard/src/pages/LandingPage/index.jsx +++ b/apps/web-dashboard/src/pages/LandingPage/index.jsx @@ -1,10 +1,9 @@ -import { useState, useEffect, useRef, useCallback } from 'react'; -import { Link, useNavigate } from 'react-router-dom'; +import { useState, useEffect } from 'react'; +import { Link, useNavigate, useLocation } from 'react-router-dom'; import { useAuth } from '../../context/AuthContext'; import BackToTop from '../../components/Layout/BackToTop'; import { Database, - Shield, HardDrive, ArrowRight, CheckCircle, @@ -24,12 +23,14 @@ import { Code, Check, Plus, - Cloud, - RefreshCw, - Server + Github, + Mail, + UserRound } from 'lucide-react'; -import { motion as Motion, AnimatePresence, useScroll, useSpring, useTransform } from 'framer-motion'; +import { motion as Motion, AnimatePresence } from 'framer-motion'; import Footer from '../../components/Layout/Footer'; +import MagicBento from '../../components/MagicBento/MagicBento'; +import Hyperspeed from '../../components/Hyperspeed/Hyperspeed'; import './style.css'; const HERO_ENDPOINTS = [ @@ -46,169 +47,597 @@ const HERO_CLICK_STEPS = [ { name: 'role', type: 'String', required: false }, ]; -const INITIAL_FEATURES = [ +const COMPATIBLE_TECHNOLOGIES = [ + // React { - id: 'database', - title: 'Managed NoSQL Database', - icon: 'Database', - tag: 'DATABASE LAYER', - desc: 'High-performance document storage powered by MongoDB. Scale from 10 to 10M records without managing servers.', - color: 'teal', - accentColor: '#00f5d4', - type: 'list', - bullets: ['Strict Type Validation', 'Auto-generated API Endpoints', 'Real-time Indexing'] + id: 'react', + label: 'React', + ring: 'inner', + cls: 'int-react', + type: 'Official SDK', + badge: '@urbackend/react', + desc: 'Connect your React applications seamlessly with our official React SDK. Equip your components with hooks, authentication providers, and state utilities.', + color: '#61DAFB', + svg: ( + + + + + + + + + ), + code: `import { UrProvider, useUrCollection } from '@urbackend/react';\n\n// 1. Wrap your app\nfunction App() {\n return (\n \n \n \n );\n}\n\n// 2. Use reactive database hooks!\nfunction TodoList() {\n const { documents, loading } = useUrCollection('todos');\n return (\n
    \n {documents.map(todo =>
  • {todo.title}
  • )}\n
\n );\n}` }, + // Next.js { - id: 'auth', - title: 'Secure Auth', - icon: 'Shield', - tag: 'IDENTITY & ACCESS', - desc: 'Full authentication flow with JWTs, BCrypt hashing, and session management built-in.', - color: 'amber', - accentColor: '#FFBD2E', - type: 'auth-info', - bullets: [] + id: 'nextjs', + label: 'Next.js', + ring: 'inner', + cls: 'int-next', + type: 'React Framework', + badge: 'SDK + Server Actions', + desc: 'Build server-rendered React applications. Works beautifully with Next.js Server Components, Middleware auth checks, and Server Actions.', + color: '#ffffff', + svg: ( + + + + + + + + + + + + + + + + ), + code: `// Get server-side data in Server Components\nimport { createServerClient } from '@urbackend/sdk/server';\n\nexport default async function DashboardPage() {\n const ur = createServerClient({\n endpoint: 'https://api.ub.bitbros.in',\n apiKey: process.env.UR_PRIVATE_KEY\n });\n\n const { data: posts } = await ur.db('posts').find();\n\n return (\n
\n {posts.map(post =>

{post.title}

)}\n
\n );\n}` }, + // Flutter { - id: 'storage', - title: 'Global Storage', - icon: 'HardDrive', - tag: 'ASSET DELIVERY', - desc: 'Upload and serve media assets via global CDN. Supports images, documents, and videos.', - color: 'blue', - accentColor: '#409EFF', - type: 'simple', - bullets: [] + id: 'flutter', + label: 'Flutter', + ring: 'inner', + cls: 'int-flutter', + type: 'Mobile Framework', + badge: 'urbackend_sdk (Dart)', + desc: 'Build gorgeous, high-performance mobile apps. Sync local collections, authenticate users, and manage media uploads natively in Dart.', + color: '#02569B', + svg: ( + + + + + ), + code: `import 'package:urbackend_sdk/urbackend_sdk.dart';\n\nvoid main() async {\n // Initialize urBackend client\n final client = UrClient(\n endpoint: 'https://api.ub.bitbros.in',\n projectId: 'proj_123',\n );\n\n // Authenticate user\n final session = await client.auth.signIn(\n email: 'user@example.com', \n password: 'secure_password'\n );\n\n print('Access Token: \${session.accessToken}');\n}` + }, + // Vue + { + id: 'vue', + label: 'Vue.js', + ring: 'inner', + cls: 'int-vue', + type: 'Frontend Framework', + badge: '@urbackend/sdk (ESM)', + desc: 'Create highly responsive web experiences. Integrates natively with Vue 3 composition APIs, reactive ref/computed state, and Pinia stores.', + color: '#42B883', + svg: ( + + + + + ), + code: `\n\n` + }, + // Webflow + { + id: 'webflow', + label: 'Webflow', + ring: 'inner', + cls: 'int-webflow', + type: 'Low-Code Web', + badge: 'Custom Script CDN', + desc: 'Add custom dynamic databases, member logic, and contact forms to Webflow designs without writing a single line of backend server code.', + color: '#4353FF', + svg: ( + + + + ), + code: `\n\n\n` + }, + // React Native + { + id: 'react-native', + label: 'React Native', + ring: 'mid', + cls: 'int-react-native', + type: 'Mobile Framework', + badge: '@urbackend/react-native', + desc: 'Write beautiful native iOS and Android apps using React and Javascript. Supports secure AsyncStorage token persistence out-of-the-box.', + color: '#61DAFB', + svg: ( + + + + + + + + + ), + code: `import { UrProvider } from '@urbackend/react';\nimport AsyncStorage from '@react-native-async-storage/async-storage';\n\n// Initialize with Native Storage for sessions\nfunction App() {\n return (\n \n \n \n );\n}` + }, + // Dart + { + id: 'dart', + label: 'Dart', + ring: 'mid', + cls: 'int-dart', + type: 'Programming Language', + badge: 'REST API Wrapper', + desc: 'Build CLI utilities, server-side tools, or Flutter extensions using raw Dart. Fast JSON processing and asynchronous networking.', + color: '#00D2B8', + svg: ( + + + + + + ), + code: `import 'package:http/http.dart' as http;\nimport 'dart:convert';\n\nvoid main() async {\n final url = Uri.parse('https://api.ub.bitbros.in/v1/db/analytics');\n final response = await http.post(\n url,\n headers: { 'x-api-key': 'ur_sec_dart' },\n body: jsonEncode({ 'metric': 'system_boot', 'value': 1 }),\n );\n print(jsonDecode(response.body));\n}` + }, + // Nuxt + { + id: 'nuxt', + label: 'Nuxt', + ring: 'mid', + cls: 'int-nuxt', + type: 'Vue Framework', + badge: 'Nuxt Module', + desc: 'Build fullstack server-rendered Vue 3 applications. Fully integrated with Nuxt server routes and asyncData fetching.', + color: '#00DC82', + svg: ( + + + + ), + code: `// server/api/users.ts\nimport { createClient } from '@urbackend/sdk';\n\nexport default defineEventHandler(async (event) => {\n const ur = createClient({\n endpoint: 'https://api.ub.bitbros.in',\n apiKey: process.env.UR_API_KEY\n });\n \n const users = await ur.collection('users').find();\n return { success: true, users };\n});` + }, + // Python + { + id: 'python', + label: 'Python', + ring: 'mid', + cls: 'int-python', + type: 'Official SDK', + badge: 'urbackend-python', + desc: 'For script automation, machine learning pipelines, and backend processing. A simple, robust requests-based client wrapper.', + color: '#FFD43B', + svg: ( + + + + + ), + code: `from urbackend import UrClient\n\n# Initialize Python Client\nclient = UrClient(\n endpoint="https://api.ub.bitbros.in",\n api_key="ur_sec_python_999"\n)\n\n# Insert documents in MongoDB\nnew_item = client.db("inventory").insert({\n "sku": "ITEM_XYZ",\n "quantity": 150,\n "location": "Warehouse A"\n})\n\nprint(f"Created doc with ID: {new_item['_id']}")` + }, + // TypeScript + { + id: 'typescript', + label: 'TypeScript', + ring: 'mid', + cls: 'int-typescript', + type: 'Official SDK', + badge: '@urbackend/sdk (Typed)', + desc: 'Lightweight core package compiled for JS/TS. Enjoy autocomplete, model schema inference, and full compiler support.', + color: '#3178C6', + svg: ( + + + + + ), + code: `import { UrClient, Document } from '@urbackend/sdk';\n\ninterface Order extends Document {\n item: string;\n total: number;\n}\n\nconst client = new UrClient({ endpoint: 'https://api.ub.bitbros.in' });\n\n// Auto-inferred typing on query responses\nconst { data } = await client.db('orders').findById('ord_999');\nconsole.log(data.item.toUpperCase());` + }, + // Swift + { + id: 'swift', + label: 'Swift', + ring: 'mid', + cls: 'int-swift', + type: 'Native iOS', + badge: 'REST API Endpoint', + desc: 'Integrate native Apple ecosystems. Fast and optimized REST routing designed for modern iOS, macOS, watchOS, or visionOS Apps.', + color: '#F05138', + svg: ( + + + + + ), + code: `import Foundation\n\nstruct User: Codable {\n let email: String\n}\n\nfunc fetchUser(userId: String) {\n let url = URL(string: "https://api.ub.bitbros.in/v1/db/users/\\(userId)")!\n var request = URLRequest(url: url)\n request.setValue("ur_sec_swift_key", forHTTPHeaderField: "x-api-key")\n \n URLSession.shared.dataTask(with: request) { data, _, _ in\n if let data = data {\n let user = try? JSONDecoder().decode(User.self, from: data)\n print("User: \\(user?.email ?? "")")\n }\n }.resume()\n}` + }, + // Kotlin + { + id: 'kotlin', + label: 'Kotlin', + ring: 'outer', + cls: 'int-kotlin', + type: 'Native Android', + badge: 'REST API Client', + desc: 'Build powerful Android applications. Fetch collections, authorize users, and write logs natively inside Kotlin-based repositories.', + color: '#7F52FF', + svg: ( + + + + ), + code: `import okhttp3.OkHttpClient\nimport okhttp3.Request\n\nval client = OkHttpClient()\n\nfun loadData() {\n val request = Request.Builder()\n .url("https://api.ub.bitbros.in/v1/db/products")\n .addHeader("x-api-key", "ur_sec_kotlin")\n .build()\n\n client.newCall(request).execute().use { response ->\n println(response.body?.string())\n }\n}` + }, + // Go + { + id: 'go', + label: 'Go', + ring: 'outer', + cls: 'int-go', + type: 'Backend Language', + badge: 'Net/HTTP API Calls', + desc: 'Connect performant microservices. urBackend gives Go services immediate read/write access to MongoDB instances over SSL endpoints.', + color: '#00ADD8', + svg: ( + + + + + ), + code: `package main\n\nimport (\n "bytes"\n "fmt"\n "net/http"\n)\n\nfunc main() {\n jsonData := []byte(\`{"device_id": "sensor_01", "temp": 24.5}\`)\n req, _ := http.NewRequest("POST", "https://api.ub.bitbros.in/v1/db/telemetry", bytes.NewBuffer(jsonData))\n req.Header.Set("x-api-key", "ur_sec_golang")\n req.Header.Set("Content-Type", "application/json")\n\n client := &http.Client{}\n resp, _ := client.Do(req)\n fmt.Println("Status Code:", resp.StatusCode)\n}` + }, + // Rust + { + id: 'rust', + label: 'Rust', + ring: 'outer', + cls: 'int-rust', + type: 'Systems Language', + badge: 'Async REST Crates', + desc: 'Integrate memory-safe microservices with urBackend. Fully asynchronous request bodies with low connection latency.', + color: '#FF6F30', + svg: ( + + + + + + ), + code: `use serde_json::json;\n\n#[tokio::main]\nasync fn main() -> Result<(), reqwest::Error> {\n let client = reqwest::Client::new();\n let body = json!({ "user_id": "usr_99", "status": "active" });\n\n let res = client.post("https://api.ub.bitbros.in/v1/db/sessions")\n .header("x-api-key", "ur_sec_rust")\n .json(&body)\n .send()\n .await?;\n\n println!("Response status: {}", res.status());\n Ok(())\n}` + }, + // FastAPI + { + id: 'fastapi', + label: 'FastAPI', + ring: 'outer', + cls: 'int-fastapi', + type: 'Python Framework', + badge: 'HTTP Client Callouts', + desc: 'Combine local python microservices with urBackend datastores. Offload authentication and file storage flows with ease.', + color: '#009485', + svg: ( + + + + ), + code: `from fastapi import FastAPI, Depends\nimport httpx\n\napp = FastAPI()\n\n@app.get("/sync-data")\nasync def sync_data():\n async with httpx.AsyncClient() as client:\n res = await client.post(\n "https://api.ub.bitbros.in/v1/db/syncs",\n headers={"x-api-key": "ur_sec_fastapi"},\n json={"status": "completed"}\n )\n return res.json()` }, + // Node.js { - id: 'architecture', - title: 'Robust Node.js Architecture', - icon: 'Cpu', - tag: 'RUNTIME CORE', - desc: 'Built on lightweight Express.js. We isolate your project to ensure consistent performance and security.', - color: 'red', - accentColor: '#FF5F56', - type: 'tags', - bullets: ['JWT Authentication', 'Lightweight'] + id: 'node', + label: 'Node.js', + ring: 'outer', + cls: 'int-node', + type: 'Core SDK', + badge: '@urbackend/sdk (ESM/CJS)', + desc: 'Perform server-side orchestration. Manage document indexing, trigger email deliveries, and query buckets directly from Node.js scripts.', + color: '#339933', + svg: ( + + + + ), + code: `const { UrClient } = require('@urbackend/sdk');\n\n// Initialize with admin privileges\nconst ur = new UrClient({\n endpoint: 'https://api.ub.bitbros.in',\n apiKey: process.env.UR_ADMIN_KEY\n});\n\nasync function sendNewsletter() {\n const result = await ur.mail.send({\n template: 'newsletter',\n to: 'subscribers@domain.com',\n data: { week: '24' }\n });\n console.log('Emails dispatched:', result.success);\n}` } ]; -const MOCK_EXTRA_FEATURES = [ +const APP_SERVICES = [ + { + id: 'auth', + title: 'User Authentication', + badge: 'IDENTITY', + desc: 'Pre-configured secure login/signup flows, session handling with JWTs, and third-party login providers (GitHub, Google) with zero setup.', + icon: UserRound, + color: '#00f5d4', + visual: ( +
+
+
+
+
+
+
+
+ Sign in to App +
+
+ user@domain.com +
+
+ +
+
+
+
G
+
+
+
+ + + +
+
+
+ ) + }, + { + id: 'db', + title: 'JSON Document Database', + badge: 'DATABASE', + desc: 'Direct database mutations and queries right from your client application. Automated validation schemas protect integrity at high speed.', + icon: Database, + color: '#FFBD2E', + visual: ( +
+
+ const user = await db.find('users'); +
+ {"{"} +
+ "id": "usr_9x", + "active": true, + "role": "member" +
+ {"}"} +
+
+
+ ) + }, + { + id: 'storage', + title: 'Secure File Storage', + badge: 'STORAGE', + desc: 'Upload, manage, and deliver media assets like avatars, documents, and videos directly. Integrated with bucket systems and global delivery networks.', + icon: HardDrive, + color: '#409EFF', + visual: ( +
+
+
+ + media-uploads + 3 files +
+
+
+
IMG
+
+ avatar.png + 1.4 MB +
+ +
+
+
DOC
+
+ invoice_q4.pdf + 340 KB +
+ +
+
+
VID
+
+ demo_clip.mp4 + + Uploading… + Done • 12 MB + +
+
+ + + + +
+
+
+
+
+
+
+
+ ) + }, + { + id: 'mail', + title: 'Transactional Mailer', + badge: 'COMMUNICATION', + desc: 'Pre-wired email dispatchers for user verification, password recovery, and system notifications. Customize layouts using pre-made email templates.', + icon: Mail, + color: '#00e676', + visual: ( +
+
+
+ + verify@urbackend.com +
+
+
Welcome! Please verify email
+
Verify Account
+
+
+ ● Sending... + ✔ Sent successfully +
+
+
+ ) + }, { - id: 'cdn', - title: 'Edge Caching & CDN', - icon: 'Cloud', - tag: 'EDGE NETWORK', - desc: 'Ultra-low latency static and dynamic content delivery closer to your users globally.', - color: 'purple', - accentColor: '#a855f7', - type: 'list', - bullets: ['Global Edge Network', 'Smart Cache Routing', 'Instant Invalidation'] + id: 'security', + title: 'Row-Level Security', + badge: 'SECURITY', + desc: 'Fine-grained read/write security configurations. Restrict access directly inside public-api based on document ownership and user authentication tokens.', + icon: Lock, + color: '#a855f7', + visual: ( +
+
RLS ENFORCED
+
+
policy "owner-write-only"
+
allow write: if owner == auth.id
+
allow read: if public
+
+
+ ) + }, + { + id: 'realtime', + title: 'Realtime Broadcast', + badge: 'REALTIME', + desc: 'Listen to database mutations instantly or broadcast custom events across clients using low-latency WebSocket connections.', + icon: Activity, + color: '#FF5F56', + visual: ( +
+
+
+ + Client A +
+
+
+
+
+ + Client B +
+
CONNECTED
+
+
+ ) } ]; -const renderFeatureIcon = (iconName) => { - switch (iconName) { - case 'Database': return ; - case 'Shield': return ; - case 'HardDrive': return ; - case 'Cpu': return ; - case 'Cloud': return ; - case 'RefreshCw': return ; - case 'Server': return ; - default: return ; +const HYPERSPEED_OPTIONS = { + distortion: 'turbulentDistortion', + length: 400, + roadWidth: 10, + islandWidth: 2, + lanesPerRoad: 3, + fov: 90, + fovSpeedUp: 150, + speedUp: 2, + carLightsFade: 0.4, + totalSideLightSticks: 20, + lightPairsPerRoadWay: 40, + shoulderLinesWidthPercentage: 0.05, + brokenLinesWidthPercentage: 0.1, + brokenLinesLengthPercentage: 0.5, + lightStickWidth: [0.12, 0.5], + lightStickHeight: [1.3, 1.7], + movingAwaySpeed: [60, 80], + movingCloserSpeed: [-120, -160], + carLightsLength: [12, 80], + carLightsRadius: [0.05, 0.14], + carWidthPercentage: [0.3, 0.5], + carShiftX: [-0.8, 0.8], + carFloorSeparation: [0, 5], + colors: { + roadColor: 0x080808, + islandColor: 0x0a0a0a, + background: 0x000000, + shoulderLines: 0x00f5d4, + brokenLines: 0x00f5d4, + leftCars: [0x00f5d4, 0x00ffd8, 0x00b4ab], + rightCars: [0x00f5d4, 0x028090, 0x00ffd8], + sticks: 0x00f5d4 } }; +const NAV_ITEMS = [ + { label: 'Features', href: '/#client-services', icon: Zap }, + { label: 'Use Cases', href: '/#use-cases', icon: Box }, + { label: 'Pricing', href: '/pricing', icon: Check }, + { label: 'Docs', href: 'https://docs.ub.bitbros.in', icon: Terminal, external: true } +]; + function LandingPage() { const { isAuthenticated } = useAuth(); const navigate = useNavigate(); + const location = useLocation(); const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false); - const [isNavVisible, setIsNavVisible] = useState(true); - const lastScrollY = useRef(0); const [scrolled, setScrolled] = useState(false); - const [openFaqIndex, setOpenFaqIndex] = useState(null); - const heroTimersRef = useRef([]); - const [collectionName, setCollectionName] = useState(''); - const [heroFields, setHeroFields] = useState([]); - const [isBuildingUi, setIsBuildingUi] = useState(false); - const [showDeploying, setShowDeploying] = useState(false); - const [showEndpoints, setShowEndpoints] = useState(false); - const [activeEndpoints, setActiveEndpoints] = useState([]); - - const timelineRef = useRef(null); - const { scrollYProgress } = useScroll({ - target: timelineRef, - offset: ["start end", "end end"] - }); - - const scaleY = useSpring(scrollYProgress, { - damping: 30, - stiffness: 100, - restDelta: 0.001 - }); - - const spineColor = useTransform( - scaleY, - [0, 0.20, 0.28, 0.53, 0.61, 0.86, 0.94, 1.0], - [ - "#00f5d4", "#00f5d4", - "#FFBD2E", "#FFBD2E", - "#409EFF", "#409EFF", - "#FF5F56", "#FF5F56" - ] - ); - - const spineGlow = useTransform( - scaleY, - [0, 0.20, 0.28, 0.53, 0.61, 0.86, 0.94, 1.0], - [ - "0 0 15px rgba(0, 245, 212, 0.6)", "0 0 15px rgba(0, 245, 212, 0.6)", - "0 0 15px rgba(255, 189, 46, 0.6)", "0 0 15px rgba(255, 189, 46, 0.6)", - "0 0 15px rgba(64, 158, 255, 0.6)", "0 0 15px rgba(64, 158, 255, 0.6)", - "0 0 15px rgba(255, 95, 86, 0.6)", "0 0 15px rgba(255, 95, 86, 0.6)" - ] - ); + useEffect(() => { + if (location.hash) { + const id = location.hash.substring(1); + const element = document.getElementById(id); + if (element) { + const timer = setTimeout(() => { + element.scrollIntoView({ behavior: 'smooth', block: 'start' }); + }, 100); + return () => clearTimeout(timer); + } + } + }, [location.pathname, location.hash]); - const [features, setFeatures] = useState(INITIAL_FEATURES); - const [isLoadingMore, setIsLoadingMore] = useState(false); - const [hasMore, setHasMore] = useState(false); - const [hoveredCardId, setHoveredCardId] = useState(null); - const [activeCardId, setActiveCardId] = useState(null); + const [openFaqIndex, setOpenFaqIndex] = useState(null); + const [hoveredTech, setHoveredTech] = useState(null); - const observerTriggerRef = useRef(null); + const [studioStep, setStudioStep] = useState(6); + const [replayKey, setReplayKey] = useState(0); useEffect(() => { - if (!hasMore) return; - - const observer = new IntersectionObserver((entries) => { - const first = entries[0]; - if (first.isIntersecting && !isLoadingMore) { - setIsLoadingMore(true); - setTimeout(() => { - setFeatures(prev => [...prev, ...MOCK_EXTRA_FEATURES]); - setIsLoadingMore(false); - setHasMore(false); - }, 1200); + if (replayKey === 0) return; + + let active = true; + const runAnimation = async () => { + setStudioStep(0); + const delays = [600, 600, 600, 700, 900, 800]; + for (let i = 0; i < delays.length; i++) { + await new Promise((resolve) => setTimeout(resolve, delays[i])); + if (!active) return; + setStudioStep(i + 1); } - }, { - root: null, - rootMargin: '150px', - threshold: 0.1 - }); - - const currentTrigger = observerTriggerRef.current; - if (currentTrigger) { - observer.observe(currentTrigger); - } - + }; + runAnimation(); return () => { - if (currentTrigger) { - observer.unobserve(currentTrigger); - } + active = false; }; - }, [isLoadingMore, hasMore]); + }, [replayKey]); + + const triggerStudioReplay = () => { + setReplayKey((prev) => prev + 1); + }; @@ -230,17 +659,7 @@ function LandingPage() { useEffect(() => { const handleScroll = () => { const currentScrollY = window.scrollY; - const delta = currentScrollY - lastScrollY.current; setScrolled(currentScrollY > 20); - - // Show quickly on even slight upward scroll; hide only on clear downward movement. - if (currentScrollY < 80 || delta < -2) { - setIsNavVisible(true); - } else if (delta > 10 && currentScrollY > 140) { - setIsNavVisible(false); - } - - lastScrollY.current = currentScrollY; }; window.addEventListener('scroll', handleScroll); @@ -250,47 +669,6 @@ function LandingPage() { return () => window.removeEventListener('scroll', handleScroll); }, []); - const clearHeroTimers = () => { - heroTimersRef.current.forEach(clearTimeout); - heroTimersRef.current = []; - }; - - const runHeroDemo = useCallback(() => { - clearHeroTimers(); - - // Wrap initial state updates in setTimeout to avoid synchronous setState inside useEffect - setTimeout(() => { - setCollectionName(''); - setHeroFields([]); - setIsBuildingUi(true); - setShowDeploying(false); - setShowEndpoints(false); - setActiveEndpoints([]); - }, 0); - - heroTimersRef.current.push(setTimeout(() => setCollectionName('users'), 400)); - heroTimersRef.current.push(setTimeout(() => setHeroFields([HERO_CLICK_STEPS[0]]), 900)); - heroTimersRef.current.push(setTimeout(() => setHeroFields([HERO_CLICK_STEPS[0], HERO_CLICK_STEPS[1]]), 1400)); - heroTimersRef.current.push(setTimeout(() => setHeroFields(HERO_CLICK_STEPS), 1900)); - heroTimersRef.current.push(setTimeout(() => setIsBuildingUi(false), 2200)); - heroTimersRef.current.push(setTimeout(() => setShowDeploying(true), 2500)); - heroTimersRef.current.push(setTimeout(() => { - setShowDeploying(false); - setShowEndpoints(true); - HERO_ENDPOINTS.forEach((_, index) => { - const timer = setTimeout(() => { - setActiveEndpoints(prev => [...prev, index]); - }, index * 160); - heroTimersRef.current.push(timer); - }); - }, 3600)); - }, []); - - useEffect(() => { - runHeroDemo(); - return () => clearHeroTimers(); - }, [runHeroDemo]); - const toggleFaq = (index) => { setOpenFaqIndex(openFaqIndex === index ? null : index); }; @@ -332,29 +710,48 @@ function LandingPage() { )}
-
diff --git a/apps/web-dashboard/src/pages/Signup.jsx b/apps/web-dashboard/src/pages/Signup.jsx index 3446f0e8d..31303b4ce 100644 --- a/apps/web-dashboard/src/pages/Signup.jsx +++ b/apps/web-dashboard/src/pages/Signup.jsx @@ -1,6 +1,6 @@ import { useEffect, useMemo, useState } from 'react'; import toast from 'react-hot-toast'; -import { Eye, EyeOff, Github, Home, Lock, Mail, UserRound } from 'lucide-react'; +import { Eye, EyeOff, Github, Lock, Mail, UserRound } from 'lucide-react'; import { useNavigate } from 'react-router-dom'; import AuthShell from '../components/AuthShell'; @@ -96,27 +96,17 @@ function Signup() { return ( navigate('/')} >
- -
- diff --git a/package-lock.json b/package-lock.json index 76c104f74..2586794be 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1151,7 +1151,9 @@ "@tanstack/react-table": "^8.21.3", "axios": "^1.13.2", "framer-motion": "^12.35.0", + "gsap": "^3.15.0", "lucide-react": "^0.554.0", + "postprocessing": "^6.39.1", "react": "^19.2.0", "react-dom": "^19.2.0", "react-hot-toast": "^2.6.0", @@ -1159,7 +1161,8 @@ "react-router-dom": "^7.9.6", "recharts": "^2.13.0", "remark-gfm": "^4.0.1", - "tailwindcss": "^4.1.18" + "tailwindcss": "^4.1.18", + "three": "^0.184.0" }, "devDependencies": { "@eslint/js": "^9.39.1", @@ -2909,7 +2912,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2943,7 +2945,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2977,7 +2978,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3043,7 +3043,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -6876,7 +6875,6 @@ "cpu": [ "ppc64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -6893,7 +6891,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -6910,7 +6907,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -6927,7 +6923,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -6944,7 +6939,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -6961,7 +6955,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -6978,7 +6971,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -6995,7 +6987,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -7012,7 +7003,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -7029,7 +7019,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -7046,7 +7035,6 @@ "cpu": [ "ia32" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -7063,7 +7051,6 @@ "cpu": [ "loong64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -7080,7 +7067,6 @@ "cpu": [ "mips64el" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -7097,7 +7083,6 @@ "cpu": [ "ppc64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -7114,7 +7099,6 @@ "cpu": [ "riscv64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -7131,7 +7115,6 @@ "cpu": [ "s390x" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -7148,7 +7131,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -7165,7 +7147,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -7182,7 +7163,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -7199,7 +7179,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -7216,7 +7195,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -7233,7 +7211,6 @@ "cpu": [ "ia32" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -8192,6 +8169,12 @@ "version": "4.2.11", "license": "ISC" }, + "node_modules/gsap": { + "version": "3.15.0", + "resolved": "https://registry.npmjs.org/gsap/-/gsap-3.15.0.tgz", + "integrity": "sha512-dMW4CWBTUK1AEEDeZc1g4xpPGIrSf9fJF960qbTZmN/QwZIWY5wgliS6JWl9/25fpTGJrMRtSjGtOmPnfjZB+A==", + "license": "Standard 'no charge' license: https://gsap.com/standard-license." + }, "node_modules/has-bigints": { "version": "1.1.0", "dev": true, @@ -11839,6 +11822,15 @@ } } }, + "node_modules/postprocessing": { + "version": "6.39.1", + "resolved": "https://registry.npmjs.org/postprocessing/-/postprocessing-6.39.1.tgz", + "integrity": "sha512-R2dG2zy+BAx3USl5EHw+PvnrlbT5PKnZVp3se0HCR0pWH8WQdh742yNG4YWOsq6c0bFpffk0Gd2RqPeoP/wKng==", + "license": "Zlib", + "peerDependencies": { + "three": ">= 0.168.0 < 0.185.0" + } + }, "node_modules/prelude-ls": { "version": "1.2.1", "dev": true, @@ -13353,6 +13345,12 @@ "node": ">=0.8" } }, + "node_modules/three": { + "version": "0.184.0", + "resolved": "https://registry.npmjs.org/three/-/three-0.184.0.tgz", + "integrity": "sha512-wtTRjG92pM5eUg/KuUnHsqSAlPM296brTOcLgMRqEeylYTh/CdtvKUvCyyCQTzFuStieWxvZb8mVTMvdPyUpxg==", + "license": "MIT" + }, "node_modules/tiny-invariant": { "version": "1.3.3", "license": "MIT"