244 lines
11 KiB
TypeScript
244 lines
11 KiB
TypeScript
import React, { useState, useRef, useLayoutEffect, useMemo } 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: 'Upgrade to Windows 11 before October 2025 to ensure continued security support and take advantage of the latest features.',
|
|
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: 'Web Services',
|
|
title: 'Web Services',
|
|
description: 'Web design, domain registration, email services, and more to establish and enhance your online presence.',
|
|
icon: 'language',
|
|
image: 'https://lh3.googleusercontent.com/aida-public/AB6AXuCxibXNCB5mU7MdWE5znMWnQUc9-d2ZoYF7LXK1CMssnvaFz2ZsGzyxXMbqDmely-UfxapqILD5-Exeo1wlQZKg8T2MK4vjlyAMaehoJoqTy2hHh8rxj46i8CKb4-ILL2JswBc98nJt_Fo1DfcDH0dHH5Zz6H4R2Jm1deViSW8Sp2zNp1sTc4eRHy1URiSRQFcr1C8rca6dKiuNDuyDiUmmesqHobXGItaBeFjJC-0OatWpKbr0zF-Y5qvk9Yl5FY2KUcDY9AcTfelu'
|
|
},
|
|
{
|
|
id: 3,
|
|
category: 'IT Infrastructure',
|
|
title: 'Performance Upgrades',
|
|
description: 'Enhance your desktops and laptops with SSDs, maintain your Windows installations, and achieve dramatic performance boosts.',
|
|
icon: 'speed',
|
|
image: 'https://lh3.googleusercontent.com/aida-public/AB6AXuBs2fGGwp4EkMarA9Uvy7IOqyW0Pzxzt-94Bsr8Tkbem4uHPq-vMEmGgKuEmds2zKwPrw2nVcvL3MjjKYWieLSLh5pVUbbK6T9aDxt2xhvo4trARZobhzoQCJfI-r6aGW_aqfwC5XxOr9VA3YdnNnYEgkfW_TWrUWYa6mD8X0KdVG3sLimA8p7qWxIqUzFFV82twn60rP4OwLdIsc6t1OGnJzjemxL1Aw05aDo6Ckfr0a1oZ2kD4xKeTkG--zUhezvXB9I03l6f3b46'
|
|
},
|
|
{
|
|
id: 4,
|
|
category: 'IT Infrastructure',
|
|
title: 'Printer & Scanner Installation',
|
|
description: 'Professional installation and configuration of printers and scanners to ensure seamless integration into your workflow.',
|
|
icon: 'print',
|
|
image: '/assets/services/printer-scanner.png'
|
|
},
|
|
{
|
|
id: 5,
|
|
category: 'IT Infrastructure',
|
|
title: 'New/Refurbished Desktop Hardware',
|
|
description: 'Supply and installation of new or refurbished desktop hardware, tailored to meet your business requirements.',
|
|
icon: 'computer',
|
|
image: '/assets/services/desktop-hardware.png'
|
|
},
|
|
{
|
|
id: 6,
|
|
category: 'Security',
|
|
title: 'VPN Setup',
|
|
description: 'Configure Virtual Private Networks to allow secure remote access to your internal network from anywhere.',
|
|
icon: 'vpn_lock',
|
|
image: '/assets/services/vpn-setup.png'
|
|
},
|
|
{
|
|
id: 7,
|
|
category: 'Networking',
|
|
title: 'Network Infrastructure Support',
|
|
description: 'Robust network solutions to ensure connectivity, security, and efficiency, including routers, access points, and switches.',
|
|
icon: 'lan',
|
|
image: '/assets/services/network-infrastructure.png'
|
|
},
|
|
{
|
|
id: 8,
|
|
category: 'Networking',
|
|
title: 'Network Attached Storage',
|
|
description: 'Selection, setup, and maintenance of Network Attached Storage solutions to provide scalable and reliable data storage.',
|
|
icon: 'storage',
|
|
image: '/assets/services/nas-storage.png'
|
|
},
|
|
{
|
|
id: 9,
|
|
category: 'IT Infrastructure',
|
|
title: 'Business IT Support',
|
|
description: 'Comprehensive IT support for businesses, including help desk, maintenance, and strategic planning.',
|
|
icon: 'business_center',
|
|
image: '/assets/services/business-it.png'
|
|
},
|
|
{
|
|
id: 10,
|
|
category: 'IT Infrastructure',
|
|
title: 'IT Help Desk',
|
|
description: 'Fast and reliable help desk support for employees, resolving technical issues remotely or on-site.',
|
|
icon: 'support_agent',
|
|
image: '/assets/services/help-desk.png'
|
|
},
|
|
{
|
|
id: 11,
|
|
category: 'IT Infrastructure',
|
|
title: 'Managed IT Services',
|
|
description: 'Proactive monitoring, security, and management of your entire IT infrastructure for a fixed monthly fee.',
|
|
icon: 'admin_panel_settings',
|
|
image: '/assets/services/managed-it.png'
|
|
}
|
|
];
|
|
|
|
const categories = ['All', 'IT Infrastructure', 'Web Services', 'Security', 'Networking'];
|
|
|
|
interface ServicesProps {
|
|
preview?: boolean;
|
|
featuredIds?: number[];
|
|
}
|
|
|
|
const Services: React.FC<ServicesProps> = ({ preview = false, featuredIds }) => {
|
|
const [activeCategory, setActiveCategory] = useState('All');
|
|
const [showAll, setShowAll] = useState(false);
|
|
const containerRef = useRef<HTMLDivElement>(null);
|
|
|
|
// Determine if we should be in "preview mode" (showing only a subset)
|
|
// This applies if preview is true OR if featuredIds are provided and we haven't clicked "Show More"
|
|
const isRestrictedView = (preview || featuredIds) && !showAll;
|
|
|
|
// Filter services based on category first (unless in restricted view with specific IDs, where we might want to ignore category or just show the specific ones)
|
|
const filteredByCategory = activeCategory === 'All'
|
|
? servicesData
|
|
: servicesData.filter(s => s.category === activeCategory || (activeCategory === 'Web Development' && s.category === 'Security'));
|
|
|
|
const displayedServices = useMemo(() => {
|
|
if (isRestrictedView) {
|
|
if (featuredIds && featuredIds.length > 0) {
|
|
// Sort the services to match the order of featuredIds
|
|
return featuredIds
|
|
.map(id => servicesData.find(s => s.id === id))
|
|
.filter((s): s is typeof servicesData[0] => s !== undefined);
|
|
}
|
|
// Fallback to first 3 if no IDs but preview is true
|
|
return servicesData.slice(0, 3);
|
|
}
|
|
// Show all (filtered by category)
|
|
return filteredByCategory;
|
|
}, [isRestrictedView, featuredIds, filteredByCategory]);
|
|
|
|
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 bg-[radial-gradient(ellipse_80%_50%_at_50%_-20%,rgba(0,0,0,0.05),rgba(0,0,0,0))] dark:bg-[radial-gradient(ellipse_80%_50%_at_50%_-20%,rgba(255,255,255,0.05),rgba(255,255,255,0))]"
|
|
>
|
|
<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">Our Services</span>
|
|
<h2 className="font-display text-3xl md:text-4xl text-gray-900 dark:text-white">
|
|
Comprehensive IT solutions <span className="text-gray-400 dark:text-gray-600">tailored to your business needs.</span>
|
|
</h2>
|
|
</div>
|
|
|
|
{/* Categories - Hide in restricted view to keep it clean, or keep it? User said "mach nur das 3 services angezeigt werden". usually categories are for the full list. */}
|
|
{!isRestrictedView && (
|
|
<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">
|
|
{displayedServices.map((service) => (
|
|
<motion.div
|
|
key={service.id}
|
|
layout
|
|
initial={{ opacity: 0 }}
|
|
animate={{ opacity: 1 }}
|
|
exit={{ opacity: 0 }}
|
|
transition={{ duration: 0.2 }}
|
|
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-gray-300 dark:hover:border-white/30 hover:shadow-2xl transition-all duration-300"
|
|
>
|
|
{/* Image Container */}
|
|
<div className="h-40 bg-gray-200 dark:bg-black/40 overflow-hidden relative">
|
|
<img
|
|
src={service.image}
|
|
alt={service.title}
|
|
className="w-full h-full object-cover transition-transform duration-700 group-hover:scale-110 opacity-100"
|
|
/>
|
|
<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-4 relative">
|
|
<motion.div
|
|
className="w-8 h-8 rounded-full bg-white dark:bg-white/10 flex items-center justify-center mb-3 border border-gray-200 dark:border-white/10"
|
|
whileHover={{ rotate: 360, backgroundColor: "#171717", color: "#ffffff", borderColor: "#171717" }}
|
|
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-lg 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-3">
|
|
{service.description}
|
|
</p>
|
|
<a href="/services" className="inline-flex items-center text-xs font-bold uppercase tracking-wide text-gray-900 dark:text-white group-hover:text-gray-600 dark:group-hover:text-gray-300 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>
|
|
|
|
{isRestrictedView && (
|
|
<div className="mt-12 text-center">
|
|
<button
|
|
onClick={() => setShowAll(true)}
|
|
className="inline-flex items-center gap-2 px-8 py-3 bg-black dark:bg-white text-white dark:text-black rounded-full font-medium hover:bg-gray-800 dark:hover:bg-gray-200 transition-colors"
|
|
>
|
|
Show More Services <span className="material-symbols-outlined text-sm">expand_more</span>
|
|
</button>
|
|
</div>
|
|
)}
|
|
|
|
{/* If we are showing all and originally had a restricted view, maybe show a "Show Less" but user didn't ask for it. The user said "then all are shown". */}
|
|
</div>
|
|
</motion.section>
|
|
);
|
|
};
|
|
|
|
export default Services; |