feat(kompetenzen): abstract ServiceDetailGrid into premium framer-motion component
Former-commit-id: 9d289c977e4b30aaaf99b5ac9cdd2899395f36ca
This commit is contained in:
178
components/blocks/ServiceDetailGrid.tsx
Normal file
178
components/blocks/ServiceDetailGrid.tsx
Normal file
@@ -0,0 +1,178 @@
|
||||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
import { motion, Variants } from 'framer-motion';
|
||||
import { HoverShineOverlay } from '@/components/ui/HoverShineOverlay';
|
||||
|
||||
export interface ServicePanelData {
|
||||
id: string;
|
||||
title: string;
|
||||
icon: 'energy' | 'fiber' | 'hdd' | 'planning' | 'survey';
|
||||
fullWidth?: boolean;
|
||||
lists: string[][];
|
||||
}
|
||||
|
||||
interface ServiceDetailGridProps {
|
||||
badge?: string;
|
||||
title?: string;
|
||||
descriptionParagraphs?: string[];
|
||||
panels: ServicePanelData[];
|
||||
}
|
||||
|
||||
const containerVariants: Variants = {
|
||||
hidden: { opacity: 0 },
|
||||
visible: {
|
||||
opacity: 1,
|
||||
transition: {
|
||||
staggerChildren: 0.1,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const itemVariants: Variants = {
|
||||
hidden: { opacity: 0, y: 40 },
|
||||
visible: { opacity: 1, y: 0, transition: { duration: 0.7, ease: [0.16, 1, 0.3, 1] } },
|
||||
};
|
||||
|
||||
const Icons = {
|
||||
energy: (
|
||||
<svg width="64" height="64" viewBox="0 0 64 64" fill="none" xmlns="http://www.w3.org/2000/svg" className="w-full h-full">
|
||||
<path d="M32 4L12 36H30L26 60L52 24H34L32 4Z" stroke="currentColor" strokeWidth="1" strokeLinejoin="miter"/>
|
||||
<circle cx="32" cy="32" r="30" stroke="currentColor" strokeWidth="1" strokeDasharray="4 4"/>
|
||||
<line x1="32" y1="2" x2="32" y2="62" stroke="currentColor" strokeWidth="0.5" strokeDasharray="2 2"/>
|
||||
<line x1="2" y1="32" x2="62" y2="32" stroke="currentColor" strokeWidth="0.5" strokeDasharray="2 2"/>
|
||||
</svg>
|
||||
),
|
||||
fiber: (
|
||||
<svg width="64" height="64" viewBox="0 0 64 64" fill="none" xmlns="http://www.w3.org/2000/svg" className="w-full h-full">
|
||||
<circle cx="32" cy="32" r="8" stroke="currentColor" strokeWidth="1"/>
|
||||
<circle cx="12" cy="16" r="4" stroke="currentColor" strokeWidth="1"/>
|
||||
<circle cx="52" cy="16" r="4" stroke="currentColor" strokeWidth="1"/>
|
||||
<circle cx="12" cy="48" r="4" stroke="currentColor" strokeWidth="1"/>
|
||||
<circle cx="52" cy="48" r="4" stroke="currentColor" strokeWidth="1"/>
|
||||
<path d="M15 19L27 28M49 19L37 28M15 45L27 36M49 45L37 36M32 40V60M32 4V24" stroke="currentColor" strokeWidth="1"/>
|
||||
<rect x="2" y="2" width="60" height="60" stroke="currentColor" strokeWidth="0.5" strokeDasharray="2 4"/>
|
||||
</svg>
|
||||
),
|
||||
hdd: (
|
||||
<svg width="64" height="64" viewBox="0 0 64 64" fill="none" xmlns="http://www.w3.org/2000/svg" className="w-full h-full">
|
||||
<path d="M4 32H20L28 20H44L52 32H60" stroke="currentColor" strokeWidth="1" strokeLinejoin="miter"/>
|
||||
<path d="M28 44H44L52 32" stroke="currentColor" strokeWidth="1" strokeLinejoin="miter" strokeDasharray="4 4"/>
|
||||
<circle cx="12" cy="32" r="2" fill="currentColor"/>
|
||||
<circle cx="36" cy="20" r="2" fill="currentColor"/>
|
||||
<circle cx="56" cy="32" r="2" fill="currentColor"/>
|
||||
<path d="M0 48H64" stroke="currentColor" strokeWidth="1"/>
|
||||
<path d="M0 54H64M0 60H64" stroke="currentColor" strokeWidth="0.5" strokeDasharray="2 2"/>
|
||||
</svg>
|
||||
),
|
||||
planning: (
|
||||
<svg width="64" height="64" viewBox="0 0 64 64" fill="none" xmlns="http://www.w3.org/2000/svg" className="w-full h-full">
|
||||
<path d="M10 54L32 10L54 54" stroke="currentColor" strokeWidth="1"/>
|
||||
<path d="M20 34H44" stroke="currentColor" strokeWidth="1"/>
|
||||
<circle cx="32" cy="10" r="4" stroke="currentColor" strokeWidth="1"/>
|
||||
<rect x="4" y="4" width="56" height="56" stroke="currentColor" strokeWidth="0.5" strokeDasharray="2 2"/>
|
||||
<line x1="4" y1="20" x2="60" y2="20" stroke="currentColor" strokeWidth="0.5" strokeDasharray="1 3"/>
|
||||
<line x1="4" y1="44" x2="60" y2="44" stroke="currentColor" strokeWidth="0.5" strokeDasharray="1 3"/>
|
||||
</svg>
|
||||
),
|
||||
survey: (
|
||||
<svg width="128" height="64" viewBox="0 0 128 64" fill="none" xmlns="http://www.w3.org/2000/svg" className="w-full h-full">
|
||||
<path d="M10 32H118" stroke="currentColor" strokeWidth="1"/>
|
||||
<path d="M64 10V54" stroke="currentColor" strokeWidth="1"/>
|
||||
<circle cx="64" cy="32" r="16" stroke="currentColor" strokeWidth="1"/>
|
||||
<circle cx="64" cy="32" r="24" stroke="currentColor" strokeWidth="0.5" strokeDasharray="4 4"/>
|
||||
<path d="M64 24L72 32L64 40L56 32L64 24Z" stroke="currentColor" strokeWidth="1"/>
|
||||
<rect x="4" y="4" width="120" height="56" stroke="currentColor" strokeWidth="0.5" strokeDasharray="2 4"/>
|
||||
<path d="M24 32V24M44 32V28M84 32V28M104 32V24" stroke="currentColor" strokeWidth="1"/>
|
||||
</svg>
|
||||
)
|
||||
};
|
||||
|
||||
export function ServiceDetailGrid({
|
||||
badge,
|
||||
title,
|
||||
descriptionParagraphs,
|
||||
panels
|
||||
}: ServiceDetailGridProps) {
|
||||
return (
|
||||
<section className="bg-neutral-dark py-24 border-b border-neutral-800 text-white relative overflow-hidden">
|
||||
{/* Premium Background Ambience */}
|
||||
<div className="absolute top-0 left-0 w-full h-[500px] bg-[radial-gradient(ellipse_at_top,_var(--tw-gradient-stops))] from-primary/10 via-neutral-dark to-neutral-dark pointer-events-none" />
|
||||
|
||||
<div className="container relative z-10 px-4">
|
||||
<div className="max-w-3xl mx-auto text-center mb-20">
|
||||
{badge && <h2 className="text-primary-light font-bold tracking-wider uppercase text-sm mb-4">{badge}</h2>}
|
||||
{title && (
|
||||
<h3 className="font-heading text-4xl md:text-5xl font-extrabold text-white mb-6">
|
||||
{title}
|
||||
</h3>
|
||||
)}
|
||||
<div className="w-20 h-1 bg-primary mx-auto mb-8 shadow-[0_0_15px_rgba(17,124,97,0.5)]"></div>
|
||||
|
||||
<div className="space-y-6 text-white/70 text-lg leading-relaxed">
|
||||
{descriptionParagraphs?.map((p, i) => {
|
||||
// Quick parse for basic bolding
|
||||
const parts = p.split('**');
|
||||
if (parts.length > 1) {
|
||||
return (
|
||||
<p key={i}>
|
||||
{parts.map((part, j) => j % 2 === 1 ? <strong key={j} className="text-white font-bold">{part}</strong> : part)}
|
||||
</p>
|
||||
);
|
||||
}
|
||||
return <p key={i}>{p}</p>;
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<motion.div
|
||||
className="grid grid-cols-1 md:grid-cols-2 gap-6"
|
||||
variants={containerVariants}
|
||||
initial="hidden"
|
||||
whileInView="visible"
|
||||
viewport={{ once: true, margin: "-50px" }}
|
||||
>
|
||||
{panels.map((panel) => {
|
||||
const isFullWidth = panel.fullWidth;
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
key={panel.id}
|
||||
variants={itemVariants}
|
||||
className={`group relative overflow-hidden bg-[#0f1d2b] border border-white/10 rounded-2xl p-8 md:p-12 transition-all duration-500 hover:border-primary/50 hover:bg-[#132333] hover:shadow-2xl select-none ${isFullWidth ? 'md:col-span-2' : ''}`}
|
||||
>
|
||||
<HoverShineOverlay shineColor="via-white/10" />
|
||||
|
||||
{/* Subtle primary glow behind the card on hover */}
|
||||
<div className="absolute inset-0 bg-primary/0 group-hover:bg-primary/5 transition-colors duration-500 z-0 pointer-events-none" />
|
||||
|
||||
<div className="relative z-10 flex flex-col md:flex-row justify-between items-start mb-10 gap-6">
|
||||
<div>
|
||||
<h4 className="text-2xl font-extrabold text-white uppercase tracking-wider">{panel.title}</h4>
|
||||
<div className="h-1 w-12 bg-primary mt-6 transition-all duration-700 group-hover:w-full group-hover:shadow-[0_0_10px_rgba(17,124,97,0.8)]"></div>
|
||||
</div>
|
||||
<div className={`text-primary shrink-0 transition-all duration-700 group-hover:scale-110 opacity-70 group-hover:opacity-100 ${isFullWidth ? 'w-32 h-16' : 'w-16 h-16'}`}>
|
||||
{Icons[panel.icon]}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={`relative z-10 grid gap-y-4 gap-x-12 ${isFullWidth ? 'md:grid-cols-2' : 'grid-cols-1'}`}>
|
||||
{panel.lists.map((list, listIdx) => (
|
||||
<ul key={listIdx} className="space-y-0 text-white/70">
|
||||
{list.map((item, itemIdx) => (
|
||||
<li key={itemIdx} className="flex items-start border-b border-white/5 py-4 last:border-b-0">
|
||||
<span className="text-primary mr-4 text-[10px] mt-2 opacity-80 group-hover:animate-pulse">■</span>
|
||||
<span className="leading-snug">{item}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
))}
|
||||
</div>
|
||||
</motion.div>
|
||||
);
|
||||
})}
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user