portfolio / src /components /ServicesSection.tsx
jashdoshi77's picture
changed content form web developemetn to data sceince
251578d
"use client";
import { useRef, useLayoutEffect, useEffect, useState } from "react";
import { motion } from "framer-motion";
import gsap from "gsap";
import { ScrollTrigger } from "gsap/ScrollTrigger";
import { section } from "framer-motion/client";
// Register GSAP plugins
if (typeof window !== "undefined") {
gsap.registerPlugin(ScrollTrigger);
}
interface Service {
icon: React.ReactNode;
title: string;
description: string;
features: string[];
}
interface CardMouseState {
x: number;
y: number;
isHovering: boolean;
}
const services: Service[] = [
{
icon: (
<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5">
<path d="M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5" />
</svg>
),
title: "Machine Learning",
description: "Building and deploying predictive models that uncover patterns and drive intelligent decision-making.",
features: ["TensorFlow / PyTorch", "Model Optimization", "MLOps"],
},
{
icon: (
<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5">
<path d="M12 19l7-7 3 3-7 7-3-3z" />
<path d="M18 13l-1.5-7.5L2 2l3.5 14.5L13 18l5-5z" />
<path d="M2 2l7.586 7.586" />
<circle cx="11" cy="11" r="2" />
</svg>
),
title: "Data Visualization",
description: "Transforming complex datasets into clear, compelling visual stories that inform and persuade.",
features: ["Tableau / Power BI", "Matplotlib / Seaborn", "Dashboard Design"],
},
{
icon: (
<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5">
<path d="M12 2v4M12 18v4M4.93 4.93l2.83 2.83M16.24 16.24l2.83 2.83M2 12h4M18 12h4M4.93 19.07l2.83-2.83M16.24 7.76l2.83-2.83" />
</svg>
),
title: "Deep Learning & NLP",
description: "Leveraging neural networks and language models to solve complex problems in vision and text understanding.",
features: ["Natural Language Processing", "Computer Vision", "Neural Networks"],
},
{
icon: (
<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5">
<rect x="2" y="3" width="20" height="14" rx="2" ry="2" />
<path d="M8 21h8M12 17v4" />
<path d="M7 8h.01M12 8h.01M17 8h.01" />
</svg>
),
title: "Data Engineering & Python Backend",
description: "End-to-end data pipelines and robust Python backend systems from ingestion to production APIs.",
features: ["SQL / NoSQL", "ETL Pipelines", "FastAPI / Flask"],
},
];
export default function ServicesSection() {
const sectionRef = useRef<HTMLElement>(null);
const headerRef = useRef<HTMLDivElement>(null);
const cardsRef = useRef<HTMLDivElement>(null);
const cardRefs = useRef<(HTMLDivElement | null)[]>([]);
const [mounted, setMounted] = useState(false);
const [isMobile, setIsMobile] = useState(false);
const [cardMouseStates, setCardMouseStates] = useState<CardMouseState[]>(
services.map(() => ({ x: 0, y: 0, isHovering: false }))
);
useEffect(() => {
const checkMobile = () => setIsMobile(window.innerWidth < 768);
checkMobile();
window.addEventListener('resize', checkMobile);
// Set mounted AFTER isMobile is determined
setMounted(true);
return () => window.removeEventListener('resize', checkMobile);
}, []);
// Track mouse position per card for 3D tilt effects
const handleMouseMove = (e: React.MouseEvent<HTMLDivElement>, cardIndex: number) => {
const card = e.currentTarget;
const rect = card.getBoundingClientRect();
const x = (e.clientX - rect.left - rect.width / 2) / (rect.width / 2);
const y = (e.clientY - rect.top - rect.height / 2) / (rect.height / 2);
setCardMouseStates(prev => {
const newStates = [...prev];
newStates[cardIndex] = { x, y, isHovering: true };
return newStates;
});
};
const handleMouseEnter = (cardIndex: number) => {
setCardMouseStates(prev => {
const newStates = [...prev];
newStates[cardIndex] = { ...newStates[cardIndex], isHovering: true };
return newStates;
});
};
const handleMouseLeave = (cardIndex: number) => {
setCardMouseStates(prev => {
const newStates = [...prev];
newStates[cardIndex] = { x: 0, y: 0, isHovering: false };
return newStates;
});
};
useLayoutEffect(() => {
if (!mounted) return;
// On mobile, skip ALL GSAP animations β€” mobile layout is pure CSS
if (isMobile) return;
const ctx = gsap.context(() => {
// Header animation
gsap.fromTo(
".services-label",
{ opacity: 0, y: 20 },
{
opacity: 1,
y: 0,
duration: 0.8,
ease: "power2.out",
scrollTrigger: {
trigger: headerRef.current,
start: "top 90%",
end: "top 70%",
scrub: 1,
},
}
);
// Heading words animation
if (headerRef.current) {
const words = headerRef.current.querySelectorAll(".word");
gsap.fromTo(
words,
{ opacity: 0, y: 40, rotateX: -15 },
{
opacity: 1,
y: 0,
rotateX: 0,
duration: 1,
stagger: 0.08,
ease: "power3.out",
scrollTrigger: {
trigger: headerRef.current,
start: "top 85%",
end: "top 55%",
scrub: 1,
},
}
);
}
// Decorative line animation
gsap.fromTo(
".services-line",
{ scaleX: 0, transformOrigin: "left center" },
{
scaleX: 1,
duration: 1.2,
ease: "power3.inOut",
scrollTrigger: {
trigger: sectionRef.current,
start: "top 80%",
end: "top 50%",
scrub: 1,
},
}
);
// Horizontal scroll animation
if (cardsRef.current && sectionRef.current) {
const scrollContainer = cardsRef.current;
const cards = scrollContainer.querySelectorAll(".service-card") as NodeListOf<HTMLElement>;
const numCards = cards.length;
if (numCards > 0) {
requestAnimationFrame(() => {
requestAnimationFrame(() => {
scrollContainer.getBoundingClientRect();
const viewportWidth = window.innerWidth;
const viewportCenter = viewportWidth / 2;
const cardWidth = cards[0].getBoundingClientRect().width;
const gap = 32;
const paddingLeft = viewportWidth * 0.08;
const firstCardCenterInitial = paddingLeft + (cardWidth / 2);
const initialOffset = firstCardCenterInitial - viewportCenter;
const lastCardLeftEdge = paddingLeft + (numCards - 1) * (cardWidth + gap);
const lastCardCenterInitial = lastCardLeftEdge + (cardWidth / 2);
const scrollAmount = lastCardCenterInitial - firstCardCenterInitial;
const scrollDistancePerCard = Math.max(400, viewportWidth * 0.4);
const totalScrollDistance = scrollDistancePerCard * numCards;
cards.forEach((card, index) => {
gsap.set(card, { willChange: "transform, opacity", force3D: true });
if (index === 0) {
gsap.set(card, { scale: 1.2, opacity: 1 });
} else {
gsap.set(card, { scale: 0.7, opacity: 0.4 });
}
});
gsap.set(scrollContainer, { x: -initialOffset, willChange: "transform", force3D: true });
gsap.fromTo(
scrollContainer,
{ x: -initialOffset },
{
x: -initialOffset - scrollAmount,
ease: "none",
immediateRender: false,
scrollTrigger: {
trigger: sectionRef.current,
start: "top top",
end: () => `+=${totalScrollDistance}`,
pin: true,
pinSpacing: true,
anticipatePin: 1,
invalidateOnRefresh: true,
refreshPriority: -1,
scrub: 1,
markers: false,
fastScrollEnd: true,
onUpdate: (self) => {
const progress = Math.min(1, Math.max(0, self.progress));
const currentX = -initialOffset - (progress * scrollAmount);
gsap.set(scrollContainer, { x: currentX });
const progressPerCard = 1 / (numCards - 1 || 1);
cards.forEach((card, index) => {
const cardCenterProgress = index * progressPerCard;
const distanceFromCenter = Math.abs(progress - cardCenterProgress);
const normalizedDistance = distanceFromCenter / progressPerCard;
const easeOut = (t: number) => 1 - (1 - t) * (1 - t);
const centeredness = easeOut(Math.max(0, 1 - normalizedDistance));
const scale = 0.7 + (0.5 * centeredness);
const opacity = 0.4 + (0.6 * centeredness);
gsap.set(card, { scale, opacity });
});
},
onLeave: () => {
gsap.set(cards[numCards - 1], { scale: 1.2, opacity: 1 });
gsap.set(scrollContainer, { x: -initialOffset - scrollAmount });
},
onEnterBack: () => {
gsap.set(cards[0], { scale: 1.2, opacity: 1 });
for (let i = 1; i < numCards; i++) {
gsap.set(cards[i], { scale: 0.7, opacity: 0.4 });
}
gsap.set(scrollContainer, { x: -initialOffset });
},
onRefresh: (self) => {
const progress = self.progress || 0;
if (progress === 0) {
gsap.set(cards[0], { scale: 1.2, opacity: 1 });
gsap.set(scrollContainer, { x: -initialOffset });
} else if (progress >= 0.99) {
gsap.set(cards[numCards - 1], { scale: 1.2, opacity: 1 });
gsap.set(scrollContainer, { x: -initialOffset - scrollAmount });
}
},
},
}
);
let resizeTimeout: NodeJS.Timeout;
const handleResize = () => {
clearTimeout(resizeTimeout);
resizeTimeout = setTimeout(() => { ScrollTrigger.refresh(); }, 300);
};
window.addEventListener("resize", handleResize);
(sectionRef.current as any)._scrollCleanup = () => {
window.removeEventListener("resize", handleResize);
cards.forEach(card => { gsap.set(card, { clearProps: "willChange" }); });
gsap.set(scrollContainer, { clearProps: "willChange" });
ScrollTrigger.getAll().forEach(trigger => {
if (trigger.vars.trigger === sectionRef.current) { trigger.kill(); }
});
};
});
});
}
}
}, sectionRef);
return () => {
ctx.revert();
if (sectionRef.current && (sectionRef.current as any)._scrollCleanup) {
(sectionRef.current as any)._scrollCleanup();
}
};
}, [mounted, isMobile]);
const headingText = "What I";
const headingText2 = "Bring to the Table";
return (
<section
id="services"
ref={sectionRef}
className="relative bg-[#050505] overflow-x-clip md:overflow-hidden"
style={{ perspective: "1000px" }}
>
{/* Full-screen dark overlay to hide other sections */}
<div className="absolute inset-0 bg-[#050505] z-0" />
{/* Background gradient orb */}
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[800px] md:w-[1200px] h-[800px] md:h-[1200px] pointer-events-none z-[1]">
<div className="absolute inset-0 bg-gradient-radial from-blue-500/8 via-purple-500/5 to-transparent blur-3xl animate-pulse" />
</div>
{/* ─── MOBILE: Vertical stacked cards (visible < 768px) ─── */}
<div className="relative z-10 px-5 py-16 md:hidden">
{/* Header */}
<div className="text-center mb-10">
<p className="text-xs font-medium text-white/40 uppercase tracking-widest mb-2">
Services
</p>
<div className="h-px w-16 bg-gradient-to-r from-transparent via-white/30 to-transparent mx-auto mb-3" />
<h2 className="text-2xl font-light text-white/90 tracking-tight leading-tight">
What I{" "}
<span className="text-white/50">Bring to the Table</span>
</h2>
</div>
{/* Vertical card grid */}
<div className="flex flex-col gap-5">
{services.map((service) => (
<div
key={service.title}
className="group relative p-5 bg-white/[0.03] border border-white/[0.08] rounded-2xl"
>
<div className="flex items-start gap-4">
{/* Icon */}
<div className="relative w-12 h-12 flex-shrink-0">
<div className="absolute inset-0 bg-gradient-to-br from-white/10 to-white/5 rounded-xl" />
<div className="absolute inset-0 flex items-center justify-center text-white/70">
{service.icon}
</div>
</div>
{/* Text */}
<div className="flex-1 min-w-0">
<h3 className="text-lg font-medium text-white/90 mb-1.5">
{service.title}
</h3>
<p className="text-sm text-white/50 leading-relaxed mb-3">
{service.description}
</p>
<div className="flex flex-wrap gap-1.5">
{service.features.map((feature) => (
<span
key={feature}
className="px-2.5 py-1 text-[11px] bg-white/5 text-white/50 rounded-full border border-white/10"
>
{feature}
</span>
))}
</div>
</div>
</div>
</div>
))}
</div>
</div>
{/* ─── DESKTOP: Horizontal scroll with GSAP pin (visible >= 768px) ─── */}
<div className="relative z-10 h-screen hidden md:flex flex-col pt-16" style={{ minHeight: "100vh" }}>
{/* Header */}
<div ref={headerRef} className="text-center mb-6 px-4 flex-shrink-0">
<p className="services-label text-sm font-medium text-white/40 uppercase tracking-widest mb-3">
Services
</p>
<div className="services-line h-px w-24 bg-gradient-to-r from-transparent via-white/30 to-transparent mx-auto mb-4" />
<h2 className="text-3xl lg:text-4xl font-light text-white/90 tracking-tight leading-tight" style={{ perspective: "1000px" }}>
{headingText.split(" ").map((word, i) => (
<span key={i} className="word inline-block mr-[0.25em]" style={{ transformStyle: "preserve-3d" }}>
{word}
</span>
))}{" "}
<span className="text-white/50">
{headingText2.split(" ").map((word, i) => (
<span key={i} className="word inline-block mr-[0.25em]" style={{ transformStyle: "preserve-3d" }}>
{word}
</span>
))}
</span>
</h2>
</div>
<div
ref={cardsRef}
className="flex gap-8 items-start py-8 flex-1 pl-[8vw] pr-[8vw]"
style={{
overflow: "visible",
willChange: "transform",
}}
>
{services.map((service, index) => {
const mouseState = cardMouseStates[index];
return (
<motion.div
key={service.title}
ref={(el) => { cardRefs.current[index] = el; }}
className="service-card group relative p-6 bg-white/[0.02] backdrop-blur-sm border border-white/5 rounded-3xl overflow-hidden cursor-pointer flex-shrink-0 w-[320px] lg:w-[350px] h-auto"
onMouseMove={(e) => handleMouseMove(e, index)}
onMouseEnter={() => handleMouseEnter(index)}
onMouseLeave={() => handleMouseLeave(index)}
style={{
transformStyle: "preserve-3d",
transform: mouseState.isHovering
? `perspective(1000px) rotateX(${-mouseState.y * 10}deg) rotateY(${mouseState.x * 10}deg) translateZ(20px)`
: "perspective(1000px) rotateX(0) rotateY(0) translateZ(0)",
transition: mouseState.isHovering
? "transform 0.1s ease-out"
: "transform 0.5s ease-out",
}}
>
<div className="card-content relative z-10">
{/* Icon */}
<div className="relative w-14 h-14 mb-5">
<div className="absolute inset-0 bg-gradient-to-br from-white/10 to-white/5 rounded-xl" />
<div className="absolute inset-0 flex items-center justify-center text-white/70 group-hover:text-white transition-colors duration-300">
{service.icon}
</div>
</div>
{/* Title */}
<h3 className="text-2xl font-medium text-white/90 mb-3 group-hover:text-white transition-colors duration-300">
{service.title}
</h3>
{/* Description */}
<p className="text-base text-white/50 leading-relaxed mb-5 group-hover:text-white/70 transition-colors duration-300">
{service.description}
</p>
{/* Features */}
<div className="flex flex-wrap gap-2">
{service.features.map((feature) => (
<span
key={feature}
className="px-3 py-1.5 text-xs bg-white/5 text-white/50 rounded-full border border-white/10 group-hover:bg-white/10 group-hover:text-white/80 group-hover:border-white/20 transition-all duration-300"
>
{feature}
</span>
))}
</div>
</div>
</motion.div>
);
})}
</div>
{/* Scroll indicator */}
<div className="flex justify-center mt-12 pb-8">
<div className="flex items-center gap-3 text-white/30">
<motion.div
animate={{ x: [0, 10, 0] }}
transition={{ duration: 1.5, repeat: Infinity, ease: "easeInOut" }}
>
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<path d="M5 12h14M12 5l7 7-7 7" />
</svg>
</motion.div>
</div>
</div>
</div>
</section>
);
}