Complete SEO overhaul

This commit is contained in:
2026-01-22 15:17:20 +01:00
parent e5c5503e08
commit 42e0971a13
37 changed files with 4319 additions and 1217 deletions

View File

@@ -1,4 +1,4 @@
import React, { useState, useRef, useLayoutEffect } from 'react';
import React, { useState, useRef, useLayoutEffect, useMemo } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import gsap from 'gsap';
import { ScrollTrigger } from 'gsap/ScrollTrigger';
@@ -69,45 +69,68 @@ const servicesData = [
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'];
const Services: React.FC<{ preview?: boolean }> = ({ preview = false }) => {
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);
const imagesRef = useRef<(HTMLDivElement | null)[]>([]);
// Reset refs on render to handle filtering updates
imagesRef.current = [];
// 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;
const filteredServices = activeCategory === 'All'
// 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 = preview ? servicesData.slice(0, 3) : filteredServices;
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]);
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
@@ -127,32 +150,35 @@ const Services: React.FC<{ preview?: boolean }> = ({ preview = false }) => {
</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>
{/* 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">
{filteredServices.map((service, index) => (
{displayedServices.map((service) => (
<motion.div
key={service.id}
layout
@@ -164,34 +190,28 @@ const Services: React.FC<{ preview?: boolean }> = ({ preview = false }) => {
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-64 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-100"
/>
</div>
<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-6 relative">
<div className="p-4 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"
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-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">
<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="#" 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">
<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] }}
@@ -204,16 +224,18 @@ const Services: React.FC<{ preview?: boolean }> = ({ preview = false }) => {
</AnimatePresence>
</div>
{preview && (
{isRestrictedView && (
<div className="mt-12 text-center">
<a
href="/services"
<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"
>
View all services <span className="material-symbols-outlined text-sm">arrow_forward</span>
</a>
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>
);