Sets up the project structure with Vite, React, and essential libraries like GSAP and Framer Motion. Configures Tailwind CSS for styling, including dark mode and custom color schemes. Includes basic component structure for the application, laying the groundwork for UI development.
167 lines
7.9 KiB
TypeScript
167 lines
7.9 KiB
TypeScript
import React, { useState, useRef, useLayoutEffect } from 'react';
|
|
import { motion, AnimatePresence } from 'framer-motion';
|
|
import gsap from 'gsap';
|
|
import { ScrollTrigger } from 'gsap/ScrollTrigger';
|
|
|
|
gsap.registerPlugin(ScrollTrigger);
|
|
|
|
const servicesData = [
|
|
{
|
|
id: 1,
|
|
category: 'IT Infrastructure',
|
|
title: 'Windows 11 Transition',
|
|
description: 'Seamless upgrades for your entire fleet. We handle compatibility checks, data backup, and deployment so your workflow never stutters.',
|
|
icon: 'desktop_windows',
|
|
image: 'https://lh3.googleusercontent.com/aida-public/AB6AXuBMpd_cFINnFibfNErBs8OVAAyDQYTRXix88YH91QImuGi11XGwlY_QUB2R9htcC1h_fTXUeftdEieGT-oi5p5TBjpAyW-86mSsXu-rqhRTBsJlAGuE37bxJES4DUayktXIToEcF-M4PyXdyyTPIYtpYrxK18b2-sPwMzuzCL0LpgJwd5EoYxAkrJQ7W4eBrIG2e9Cw9sY0dJpXJy-TRgwBG0nk-S7W4Y0s3U9w--AzE4fcUimeGMqWwdCncU5tnETmkrkDNFiCyKSA'
|
|
},
|
|
{
|
|
id: 2,
|
|
category: 'Security',
|
|
title: 'Web Services & Security',
|
|
description: 'From hosting to rigorous penetration testing. Secure your digital storefront with enterprise-grade protection and 99.9% uptime.',
|
|
icon: 'security',
|
|
image: 'https://lh3.googleusercontent.com/aida-public/AB6AXuCxibXNCB5mU7MdWE5znMWnQUc9-d2ZoYF7LXK1CMssnvaFz2ZsGzyxXMbqDmely-UfxapqILD5-Exeo1wlQZKg8T2MK4vjlyAMaehoJoqTy2hHh8rxj46i8CKb4-ILL2JswBc98nJt_Fo1DfcDH0dHH5Zz6H4R2Jm1deViSW8Sp2zNp1sTc4eRHy1URiSRQFcr1C8rca6dKiuNDuyDiUmmesqHobXGItaBeFjJC-0OatWpKbr0zF-Y5qvk9Yl5FY2KUcDY9AcTfelu'
|
|
},
|
|
{
|
|
id: 3,
|
|
category: 'Consulting',
|
|
title: 'Performance Upgrades',
|
|
description: 'Is your hardware holding you back? We analyze bottlenecks and implement strategic upgrades to memory, storage, and networks.',
|
|
icon: 'speed',
|
|
image: 'https://lh3.googleusercontent.com/aida-public/AB6AXuBs2fGGwp4EkMarA9Uvy7IOqyW0Pzxzt-94Bsr8Tkbem4uHPq-vMEmGgKuEmds2zKwPrw2nVcvL3MjjKYWieLSLh5pVUbbK6T9aDxt2xhvo4trARZobhzoQCJfI-r6aGW_aqfwC5XxOr9VA3YdnNnYEgkfW_TWrUWYa6mD8X0KdVG3sLimA8p7qWxIqUzFFV82twn60rP4OwLdIsc6t1OGnJzjemxL1Aw05aDo6Ckfr0a1oZ2kD4xKeTkG--zUhezvXB9I03l6f3b46'
|
|
}
|
|
];
|
|
|
|
const categories = ['All', 'IT Infrastructure', 'Web Development', 'Consulting', 'Security'];
|
|
|
|
const Services: React.FC = () => {
|
|
const [activeCategory, setActiveCategory] = useState('All');
|
|
const containerRef = useRef<HTMLDivElement>(null);
|
|
const imagesRef = useRef<(HTMLDivElement | null)[]>([]);
|
|
|
|
// Reset refs on render to handle filtering updates
|
|
imagesRef.current = [];
|
|
|
|
const filteredServices = activeCategory === 'All'
|
|
? servicesData
|
|
: servicesData.filter(s => s.category === activeCategory || (activeCategory === 'Web Development' && s.category === 'Security'));
|
|
|
|
useLayoutEffect(() => {
|
|
const ctx = gsap.context(() => {
|
|
imagesRef.current.forEach((imgWrapper) => {
|
|
if (!imgWrapper) return;
|
|
|
|
gsap.to(imgWrapper, {
|
|
yPercent: 30,
|
|
ease: "none",
|
|
scrollTrigger: {
|
|
trigger: imgWrapper.closest('.group'),
|
|
start: "top bottom",
|
|
end: "bottom top",
|
|
scrub: true
|
|
}
|
|
});
|
|
});
|
|
}, containerRef);
|
|
|
|
return () => ctx.revert();
|
|
}, [filteredServices]);
|
|
|
|
return (
|
|
<motion.section
|
|
ref={containerRef}
|
|
id="services"
|
|
initial={{ opacity: 0, y: 50 }}
|
|
whileInView={{ opacity: 1, y: 0 }}
|
|
viewport={{ once: true, margin: "-100px" }}
|
|
transition={{ duration: 0.8, ease: "easeOut" }}
|
|
className="py-24 bg-white dark:bg-[#0f0f0f] border-t border-gray-100 dark:border-white/5"
|
|
>
|
|
<div className="max-w-7xl mx-auto px-6">
|
|
<div className="mb-16">
|
|
<span className="text-xs font-semibold uppercase tracking-widest text-gray-500 dark:text-gray-500 mb-2 block">Core Offerings</span>
|
|
<h2 className="font-display text-3xl md:text-4xl text-gray-900 dark:text-white">
|
|
Different paths to explore <span className="text-gray-400 dark:text-gray-600">all guided by one expert team.</span>
|
|
</h2>
|
|
</div>
|
|
|
|
<div className="flex gap-6 mb-12 border-b border-gray-200 dark:border-white/10 text-sm font-medium overflow-x-auto pb-2 no-scrollbar">
|
|
{categories.map((cat) => (
|
|
<button
|
|
key={cat}
|
|
onClick={() => setActiveCategory(cat)}
|
|
className={`pb-2 whitespace-nowrap transition-colors relative ${
|
|
activeCategory === cat
|
|
? 'text-gray-900 dark:text-white'
|
|
: 'text-gray-500 dark:text-gray-500 hover:text-gray-800 dark:hover:text-gray-300'
|
|
}`}
|
|
>
|
|
{cat}
|
|
{activeCategory === cat && (
|
|
<motion.div
|
|
layoutId="activeTab"
|
|
className="absolute bottom-0 left-0 right-0 h-0.5 bg-black dark:bg-white"
|
|
/>
|
|
)}
|
|
</button>
|
|
))}
|
|
</div>
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
|
<AnimatePresence mode="popLayout">
|
|
{filteredServices.map((service) => (
|
|
<motion.div
|
|
key={service.id}
|
|
layout
|
|
initial={{ opacity: 0, scale: 0.9 }}
|
|
animate={{ opacity: 1, scale: 1 }}
|
|
exit={{ opacity: 0, scale: 0.9 }}
|
|
whileHover={{ y: -10, transition: { duration: 0.3 } }}
|
|
className="group relative bg-gray-50 dark:bg-white/5 border border-gray-200 dark:border-white/10 rounded-xl overflow-hidden hover:border-blue-500/50 dark:hover:border-blue-500/50 hover:shadow-2xl transition-all duration-300"
|
|
>
|
|
{/* Image Container */}
|
|
<div className="h-48 bg-gray-200 dark:bg-black/40 overflow-hidden relative">
|
|
{/* Parallax Wrapper */}
|
|
<div
|
|
ref={el => { if (el) imagesRef.current.push(el); }}
|
|
className="w-full h-[140%] -mt-[20%]"
|
|
>
|
|
<img
|
|
src={service.image}
|
|
alt={service.title}
|
|
className="w-full h-full object-cover transition-transform duration-700 group-hover:scale-110 opacity-80 group-hover:opacity-100"
|
|
/>
|
|
</div>
|
|
<div className="absolute inset-0 bg-gradient-to-t from-gray-50 dark:from-[#161616] to-transparent pointer-events-none"></div>
|
|
</div>
|
|
|
|
<div className="p-6 relative">
|
|
<motion.div
|
|
className="w-10 h-10 rounded-full bg-white dark:bg-white/10 flex items-center justify-center mb-4 border border-gray-200 dark:border-white/10"
|
|
whileHover={{ rotate: 360, backgroundColor: "#3b82f6", color: "white", borderColor: "#3b82f6" }}
|
|
transition={{ duration: 0.5 }}
|
|
>
|
|
<span className="material-symbols-outlined text-sm text-gray-900 dark:text-white group-hover:text-white">{service.icon}</span>
|
|
</motion.div>
|
|
<h3 className="font-display text-xl font-bold text-gray-900 dark:text-white mb-2">{service.title}</h3>
|
|
<p className="text-sm text-gray-600 dark:text-gray-400 leading-relaxed mb-4">
|
|
{service.description}
|
|
</p>
|
|
<a href="#" className="inline-flex items-center text-xs font-bold uppercase tracking-wide text-gray-900 dark:text-white group-hover:text-blue-500 transition-colors">
|
|
Learn More <motion.span
|
|
className="material-symbols-outlined text-xs ml-1"
|
|
animate={{ x: [0, 5, 0] }}
|
|
transition={{ repeat: Infinity, duration: 1.5, ease: "easeInOut", repeatDelay: 1 }}
|
|
>arrow_forward</motion.span>
|
|
</a>
|
|
</div>
|
|
</motion.div>
|
|
))}
|
|
</AnimatePresence>
|
|
</div>
|
|
</div>
|
|
</motion.section>
|
|
);
|
|
};
|
|
|
|
export default Services; |