From 34e409123fc90bad3552d0866aed378d7090c95f Mon Sep 17 00:00:00 2001 From: Mohammad Abir Abbas aka uknowwhoab1r <66947064+mdabir1203@users.noreply.github.com> Date: Sat, 1 Nov 2025 11:52:55 +0500 Subject: [PATCH] fix: import fragment for cursor trail --- package.json | 1 + public/images/trail-aurora.svg | 10 + public/images/trail-gem.svg | 11 + public/images/trail-spark.svg | 11 + src/components/CursorTrail.tsx | 275 ++++++++++++++++++++++ src/components/HomeSection.tsx | 215 ++++++++++------- src/components/JourneySection.tsx | 268 +++++++++++++++++---- src/components/ProjectsSection.tsx | 107 +++++---- src/components/SocialPresenceShowcase.tsx | 167 ++++++------- src/index.css | 71 ++++++ yarn.lock | 46 ++++ 11 files changed, 922 insertions(+), 260 deletions(-) create mode 100644 public/images/trail-aurora.svg create mode 100644 public/images/trail-gem.svg create mode 100644 public/images/trail-spark.svg create mode 100644 src/components/CursorTrail.tsx diff --git a/package.json b/package.json index f674bb8a..217fb243 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "dotenv": "^16.4.7", "express": "^4.21.2", "express-rate-limit": "^8.1.0", + "framer-motion": "^12.23.24", "langchain": "^0.3.13", "react": "^18.3.1", "react-dom": "^18.3.1", diff --git a/public/images/trail-aurora.svg b/public/images/trail-aurora.svg new file mode 100644 index 00000000..2e82b0e5 --- /dev/null +++ b/public/images/trail-aurora.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/public/images/trail-gem.svg b/public/images/trail-gem.svg new file mode 100644 index 00000000..bce149e5 --- /dev/null +++ b/public/images/trail-gem.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/public/images/trail-spark.svg b/public/images/trail-spark.svg new file mode 100644 index 00000000..530ddc38 --- /dev/null +++ b/public/images/trail-spark.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/src/components/CursorTrail.tsx b/src/components/CursorTrail.tsx new file mode 100644 index 00000000..861fbf7c --- /dev/null +++ b/src/components/CursorTrail.tsx @@ -0,0 +1,275 @@ +import React, { + CSSProperties, + FC, + ReactNode, + useCallback, + useEffect, + useMemo, + useRef, + useState, +} from 'react'; + +interface CursorTrailProps { + images: string[]; + children: ReactNode; + className?: string; + spawnInterval?: number; + maxItems?: number; + fadeDuration?: number; +} + +interface TrailItem { + id: number; + x: number; + y: number; + image: string; + createdAt: number; + size: number; + rotation: number; +} + +type TrailStyle = CSSProperties & { + '--cursor-trail-rotate'?: string; + '--cursor-trail-duration'?: string; +}; + +type RevealStyle = CSSProperties & { + '--cursor-trail-duration'?: string; +}; + +type VeilStyle = CSSProperties & { + '--cursor-trail-veil-opacity'?: string; +}; + +const DEFAULT_SPAWN_INTERVAL = 70; +const DEFAULT_MAX_ITEMS = 28; +const DEFAULT_FADE_DURATION = 900; + +const CursorTrail: FC = ({ + images, + children, + className, + spawnInterval = DEFAULT_SPAWN_INTERVAL, + maxItems = DEFAULT_MAX_ITEMS, + fadeDuration = DEFAULT_FADE_DURATION, +}) => { + const [items, setItems] = useState([]); + const lastSpawnRef = useRef(0); + const idRef = useRef(0); + const containerRef = useRef(null); + const isInsideRef = useRef(false); + const rectRef = useRef(null); + const rafRef = useRef(); + + const chosenImages = useMemo(() => images.filter(Boolean), [images]); + const hasImages = chosenImages.length > 0; + + useEffect(() => { + if (!hasImages) { + return undefined; + } + + const prune = () => { + const now = performance.now(); + setItems((current) => { + const filtered = current.filter((item) => now - item.createdAt < fadeDuration); + if (filtered.length > 0) { + rafRef.current = window.requestAnimationFrame(prune); + } else { + rafRef.current = undefined; + } + return filtered; + }); + }; + + if (items.length > 0 && rafRef.current === undefined) { + rafRef.current = window.requestAnimationFrame(prune); + } + + return () => { + if (rafRef.current !== undefined) { + window.cancelAnimationFrame(rafRef.current); + rafRef.current = undefined; + } + }; + }, [fadeDuration, hasImages, items.length]); + + const updateRect = useCallback(() => { + const node = containerRef.current; + if (!node) { + rectRef.current = null; + return; + } + + rectRef.current = node.getBoundingClientRect(); + }, []); + + useEffect(() => { + if (!hasImages) { + return undefined; + } + + updateRect(); + + const handleResize = () => updateRect(); + window.addEventListener('resize', handleResize); + window.addEventListener('scroll', handleResize, true); + + return () => { + window.removeEventListener('resize', handleResize); + window.removeEventListener('scroll', handleResize, true); + }; + }, [hasImages, updateRect]); + + const spawnItem = useCallback( + (clientX: number, clientY: number) => { + if (!hasImages) { + return; + } + + const now = performance.now(); + if (now - lastSpawnRef.current < spawnInterval) { + return; + } + + const rect = rectRef.current ?? containerRef.current?.getBoundingClientRect(); + if (!rect) { + return; + } + + if ( + clientX < rect.left || + clientX > rect.right || + clientY < rect.top || + clientY > rect.bottom + ) { + return; + } + + lastSpawnRef.current = now; + + const x = clientX - rect.left; + const y = clientY - rect.top; + + const image = chosenImages[Math.floor(Math.random() * chosenImages.length)]; + const size = 36 + Math.random() * 32; + const rotation = -22 + Math.random() * 44; + + setItems((current) => { + const filtered = current.filter((item) => now - item.createdAt < fadeDuration); + const nextItems = [ + ...filtered, + { + id: idRef.current++, + x, + y, + image, + createdAt: now, + size, + rotation, + }, + ]; + + if (nextItems.length > maxItems) { + return nextItems.slice(nextItems.length - maxItems); + } + + return nextItems; + }); + }, + [chosenImages, fadeDuration, hasImages, maxItems, spawnInterval], + ); + + const handlePointerEnter = useCallback( + (event: React.PointerEvent) => { + isInsideRef.current = true; + updateRect(); + spawnItem(event.clientX, event.clientY); + }, + [spawnItem, updateRect], + ); + + const handlePointerLeave = useCallback(() => { + isInsideRef.current = false; + setItems([]); + if (rafRef.current !== undefined) { + window.cancelAnimationFrame(rafRef.current); + rafRef.current = undefined; + } + }, []); + + useEffect(() => { + if (!hasImages) { + return undefined; + } + + const handleWindowPointerMove = (event: PointerEvent) => { + if (!isInsideRef.current) { + return; + } + + spawnItem(event.clientX, event.clientY); + }; + + window.addEventListener('pointermove', handleWindowPointerMove, { passive: true }); + + return () => { + window.removeEventListener('pointermove', handleWindowPointerMove); + }; + }, [hasImages, spawnItem]); + + const outerClassName = ['relative block isolate', className].filter(Boolean).join(' '); + const veilOpacity = items.length > 0 ? '0.56' : '0.26'; + const veilStyle: VeilStyle = { + '--cursor-trail-veil-opacity': veilOpacity, + }; + + return ( +
+
{children}
+
+ + ); +}; + +export default CursorTrail; diff --git a/src/components/HomeSection.tsx b/src/components/HomeSection.tsx index c0a8bcbb..86db71d0 100644 --- a/src/components/HomeSection.tsx +++ b/src/components/HomeSection.tsx @@ -1,5 +1,6 @@ import { FC, memo } from 'react'; import { LinkedInRecommendation } from '../data/linkedin-recommendations'; +import CursorTrail from './CursorTrail'; import SocialPresenceShowcase from './SocialPresenceShowcase'; interface HomeSectionProps { @@ -8,108 +9,152 @@ interface HomeSectionProps { linkedinRecommendations: LinkedInRecommendation[]; } +const trailImages = ['/images/trail-gem.svg', '/images/trail-spark.svg', '/images/trail-aurora.svg']; + const HomeSection: FC = ({ onHireClick, isHired, linkedinRecommendations }) => { return ( -
-
-
-
-
-
- -
-
-
-
- Mohammad Abir Abbas + +
+
+
+
+
+
+
+ MOHAMMAD +
+
+ ABBAS
+
+ Work + About + Thoughts + Contact +
+
+ Portfolio 2K24 +
+
-
-

- Mohammad Abir Abbas -

-
-

- We craft AI-powered, secure, and scalable solutions that drive impact. From multi-LLM workflows to Rust systems, every decision is data-driven, every product human-focused, and every line of code aimed at breaking barriers and shaping the impossible. -

+
+
+
+ + + Strategic technologist & storyteller + +

+ Mohammad Abir Abbas +

+

+ We craft AI-powered, secure, and scalable solutions that drive impact. From multi-LLM workflows to Rust systems, every decision is data-driven, every product human-focused, and every line of code aimed at breaking barriers and shaping the impossible. +

+
+ +
+
+ + AI Whisperer + + + Rust Artisan + + + Vibe Coder + +
+ +
+ +
+
+

Focus

+

Multi-LLM Systems

+
+
+

Edge

+

Secure Rust Pipelines

+
+
+

Promise

+

Human-Centered Results

+
+
-
-
- - AI Whisperer - - - Rust Artisan - - - Vibe Coder - +
+
+
+
+
+
+ Mohammad Abir Abbas +
2K24
+
+ Future Ready +
+
-
-
- + -
-
-

- LinkedIn Recommendations -

-
- {linkedinRecommendations.map((recommendation, index) => ( -
-
- {recommendation.name} -
-

{recommendation.name}

-

{recommendation.role}

+
+
+

+ LinkedIn Recommendations +

+
+ {linkedinRecommendations.map((recommendation, index) => ( +
+
+ {recommendation.name} +
+

{recommendation.name}

+

{recommendation.role}

+
+
+

"{recommendation.content}"

+
+ {[...Array(5)].map((_, i) => ( + + + + ))}
-

"{recommendation.content}"

-
- {[...Array(5)].map((_, i) => ( - - - - ))} -
-
- ))} -
- +
-
+
-
+ ); }; diff --git a/src/components/JourneySection.tsx b/src/components/JourneySection.tsx index 010b75b7..be24c206 100644 --- a/src/components/JourneySection.tsx +++ b/src/components/JourneySection.tsx @@ -1,49 +1,231 @@ -import { FC, memo } from 'react'; +import { FC, memo, useCallback, useMemo, useRef } from 'react'; +import type { PointerEvent as ReactPointerEvent } from 'react'; +import { motion, useMotionValue, useScroll, useSpring, useTransform, MotionValue } from 'framer-motion'; +import clsx from 'clsx'; import { JourneyStep } from '../data/journey'; -interface JourneySectionProps { +type JourneySectionProps = { journey: JourneyStep[]; -} - -const JourneySection: FC = ({ journey }) => ( -
-

- My Digital Journey -

- -
- {journey.map((step, index) => ( -
-
-
-
{step.year}
-

{step.title}

-

{step.description}

-
-
- -
-
- {index < journey.length - 1 && ( -
- )} -
- -
+}; + +type ParticleConfig = { + left: number; + top: number; + size: number; + blur: number; + floatDistance: number; + duration: number; + delay: number; + parallaxIntensity: number; + opacity: number; +}; + +type TimelineParticleProps = { + config: ParticleConfig; + pointerX: MotionValue; + pointerY: MotionValue; +}; + +const TimelineParticle = memo(({ config, pointerX, pointerY }: TimelineParticleProps) => { + const parallaxX = useTransform(pointerX, (value) => (value - 0.5) * config.parallaxIntensity); + const parallaxY = useTransform(pointerY, (value) => (value - 0.5) * config.parallaxIntensity * 1.2); + + return ( + + ); +}); + +TimelineParticle.displayName = 'TimelineParticle'; + +const JourneySection: FC = ({ journey }) => { + const sectionRef = useRef(null); + const pointerX = useMotionValue(0.5); + const pointerY = useMotionValue(0.5); + const smoothPointerX = useSpring(pointerX, { stiffness: 90, damping: 18, mass: 0.4 }); + const smoothPointerY = useSpring(pointerY, { stiffness: 90, damping: 18, mass: 0.4 }); + + const { scrollYProgress } = useScroll({ + target: sectionRef, + offset: ['start 80%', 'end start'] + }); + + const lineGlow = useTransform(scrollYProgress, [0, 1], [0.35, 1]); + const haloScale = useTransform(scrollYProgress, [0, 1], [0.8, 1.05]); + const haloOpacity = useTransform(scrollYProgress, [0, 1], [0.15, 0.35]); + + const particles = useMemo( + () => [ + { left: 14, top: 8, size: 220, blur: 55, floatDistance: 36, duration: 16, delay: 0, parallaxIntensity: 32, opacity: 0.6 }, + { left: 78, top: 14, size: 160, blur: 40, floatDistance: 28, duration: 18, delay: 1.8, parallaxIntensity: 26, opacity: 0.5 }, + { left: 28, top: 42, size: 180, blur: 48, floatDistance: 34, duration: 20, delay: 0.6, parallaxIntensity: 30, opacity: 0.55 }, + { left: 66, top: 54, size: 210, blur: 60, floatDistance: 46, duration: 22, delay: 1.2, parallaxIntensity: 28, opacity: 0.45 }, + { left: 18, top: 72, size: 190, blur: 52, floatDistance: 38, duration: 19, delay: 2.4, parallaxIntensity: 34, opacity: 0.5 }, + { left: 74, top: 82, size: 200, blur: 58, floatDistance: 40, duration: 24, delay: 1.4, parallaxIntensity: 24, opacity: 0.6 } + ], + [] + ); + + const handlePointerMove = useCallback( + (event: ReactPointerEvent) => { + const bounds = sectionRef.current?.getBoundingClientRect(); + if (!bounds) { + return; + } + + const relativeX = (event.clientX - bounds.left) / bounds.width; + const relativeY = (event.clientY - bounds.top) / bounds.height; + + pointerX.set(Math.min(Math.max(relativeX, 0), 1)); + pointerY.set(Math.min(Math.max(relativeY, 0), 1)); + }, + [pointerX, pointerY] + ); + + const handlePointerLeave = useCallback(() => { + pointerX.set(0.5); + pointerY.set(0.5); + }, [pointerX, pointerY]); + + return ( + +
+ + + +
+ {particles.map((config, index) => ( + + ))} +
+ +
+
+ + My Digital Journey + +

+ A curated timeline of pivotal chapters—each milestone harmonised across craft, community, and cutting-edge creation. +

- ))} -
- -
-

Builder Philosophy

-

- "We build boldly, break fearlessly, and aim for horizons yet unseen" -

-
-
-); + +
+ + + + + {journey.map((step, index) => { + const isLeftAligned = index % 2 === 0; + + return ( + +