Complete SEO overhaul
This commit is contained in:
@@ -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>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user