feat: V2/V3组件体系与水墨雅致视觉改造

- 新增detail-v2组件库(HeroV2/V3, TrustSectionV2, CTASectionV2等)
- Hero组件水墨雅致改造:浅色宣纸底/深色墨色文字/墨韵纹理
- 方案卡片添加推荐组合徽标(Package图标)
- 独立产品页从V1组件迁移到V2/V3
- 修复why-us-section未使用导入和ESLint引号转义错误
This commit is contained in:
张翔
2026-06-07 16:19:44 +08:00
parent 2d602c0e57
commit 724a00f582
32 changed files with 3690 additions and 87 deletions
@@ -0,0 +1,38 @@
'use client';
import { motion } from 'framer-motion';
import type { CaseStudy } from '@/lib/constants/products';
interface CaseStudyCardProps {
study: CaseStudy;
index: number;
}
export function CaseStudyCard({ study, index }: CaseStudyCardProps) {
return (
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: '-50px' }}
transition={{ duration: 0.5, delay: index * 0.1 }}
className="rounded-xl border border-gray-200 bg-white p-6 shadow-sm transition-shadow hover:shadow-md"
>
<div className="mb-3 flex items-center gap-2">
<span className="rounded-full bg-red-50 px-2.5 py-1 text-xs font-medium text-[#C41E3A]">
{study.industry}
</span>
<span className="text-sm text-gray-500">{study.client}</span>
</div>
<h4 className="mb-2 text-sm font-semibold text-gray-900"></h4>
<p className="mb-4 text-sm leading-relaxed text-gray-600">{study.challenge}</p>
<h4 className="mb-2 text-sm font-semibold text-gray-900"></h4>
<p className="mb-4 text-sm leading-relaxed text-gray-600">{study.solution}</p>
<div className="rounded-lg bg-green-50 p-3">
<p className="text-sm font-medium text-green-800">{study.result}</p>
</div>
</motion.div>
);
}
@@ -0,0 +1,36 @@
'use client';
import { motion } from 'framer-motion';
import { Award, ExternalLink } from 'lucide-react';
import type { Certification } from '@/lib/constants/products';
interface CertificationListProps {
certifications: Certification[];
}
export function CertificationList({ certifications }: CertificationListProps) {
if (certifications.length === 0) return null;
return (
<div className="flex flex-wrap gap-3">
{certifications.map((cert, index) => (
<motion.a
key={cert.name}
href={cert.link || '#'}
initial={{ opacity: 0, scale: 0.95 }}
whileInView={{ opacity: 1, scale: 1 }}
viewport={{ once: true, margin: '-50px' }}
transition={{ duration: 0.3, delay: index * 0.05 }}
className="inline-flex items-center gap-2 rounded-full border border-gray-200 bg-white px-4 py-2 text-sm text-gray-700 shadow-sm transition-all hover:border-gray-300 hover:shadow-md"
>
<Award className="h-4 w-4 text-[#C41E3A]" />
<span className="font-medium">{cert.name}</span>
<span className="text-xs text-gray-400">{cert.issuer}</span>
{cert.link && cert.link !== '#' && (
<ExternalLink className="h-3 w-3 text-gray-400" />
)}
</motion.a>
))}
</div>
);
}
@@ -0,0 +1,103 @@
'use client';
import { motion } from 'framer-motion';
import { ArrowRight, Package, Lightbulb, Wrench } from 'lucide-react';
import type { CrossReference } from '@/lib/constants/cross-references';
interface CrossRecommendGridProps {
items: CrossReference[];
title?: string;
}
const typeConfig = {
product: {
icon: Package,
label: '产品',
color: '#3b82f6',
bg: '#eff6ff',
},
solution: {
icon: Lightbulb,
label: '方案',
color: '#C41E3A',
bg: '#fef2f2',
},
service: {
icon: Wrench,
label: '服务',
color: '#7c3aed',
bg: '#f5f3ff',
},
};
export function CrossRecommendGrid({ items, title = '相关推荐' }: CrossRecommendGridProps) {
if (items.length === 0) return null;
return (
<section className="bg-white py-16 lg:py-20">
<div className="container-wide">
<motion.h2
initial={{ opacity: 0 }}
whileInView={{ opacity: 1 }}
viewport={{ once: true }}
className="mb-2 text-center text-2xl font-bold text-gray-900"
>
{title}
</motion.h2>
<motion.p
initial={{ opacity: 0 }}
whileInView={{ opacity: 1 }}
viewport={{ once: true }}
transition={{ delay: 0.1 }}
className="mx-auto mb-8 max-w-2xl text-center text-sm text-gray-500"
>
</motion.p>
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3">
{items.map((item, index) => {
const config = typeConfig[item.type];
const Icon = config.icon;
return (
<motion.a
key={item.id}
href={item.href}
initial={{ opacity: 0, y: 16 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.4, delay: index * 0.08 }}
className="group flex items-start gap-4 rounded-xl border border-gray-100 bg-gray-50/50 p-5 transition-all hover:border-gray-200 hover:bg-white hover:shadow-md"
>
<div
className="flex h-10 w-10 shrink-0 items-center justify-center rounded-lg"
style={{ backgroundColor: config.bg, color: config.color }}
>
<Icon className="h-5 w-5" />
</div>
<div className="min-w-0 flex-1">
<div className="mb-1 flex items-center gap-2">
<span
className="rounded-full px-2 py-0.5 text-xs font-medium"
style={{
color: config.color,
backgroundColor: `${config.color}15`,
}}
>
{config.label}
</span>
<ArrowRight className="h-3 w-3 text-gray-400 opacity-0 transition-opacity group-hover:opacity-100" />
</div>
<h3 className="text-sm font-semibold text-gray-900">{item.title}</h3>
<p className="mt-1 text-xs leading-relaxed text-gray-500">
{item.reason}
</p>
</div>
</motion.a>
);
})}
</div>
</div>
</section>
);
}
@@ -0,0 +1,60 @@
'use client';
import { motion } from 'framer-motion';
import { ArrowRight } from 'lucide-react';
import type { HeroTheme } from '@/lib/constants/hero-themes';
interface DetailCTASectionProps {
theme: HeroTheme;
title?: string;
description?: string;
primaryAction: { label: string; href: string };
secondaryAction?: { label: string; href: string };
}
export function DetailCTASection({
theme,
title = '准备好开始了吗?',
description = '联系我们,获取专属方案和报价',
primaryAction,
secondaryAction,
}: DetailCTASectionProps) {
return (
<section
className="py-16 lg:py-20"
style={{
background: `linear-gradient(135deg, ${theme.gradientFrom}, ${theme.gradientTo})`,
}}
>
<div className="container-wide text-center">
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.5 }}
>
<h2 className="text-2xl font-bold text-white lg:text-3xl">{title}</h2>
<p className="mx-auto mt-3 max-w-xl text-sm text-white/70">{description}</p>
<div className="mt-8 flex flex-wrap justify-center gap-4">
<a
href={primaryAction.href}
className="inline-flex items-center gap-2 rounded-lg bg-white px-8 py-3 text-sm font-semibold text-gray-900 shadow-lg transition-all hover:shadow-xl"
>
{primaryAction.label}
<ArrowRight className="h-4 w-4" />
</a>
{secondaryAction && (
<a
href={secondaryAction.href}
className="inline-flex items-center rounded-lg border border-white/30 px-6 py-3 text-sm font-semibold text-white transition-all hover:bg-white/10"
>
{secondaryAction.label}
</a>
)}
</div>
</motion.div>
</div>
</section>
);
}
@@ -0,0 +1,35 @@
'use client';
import { motion } from 'framer-motion';
import type { DataProof } from '@/lib/constants/products';
interface DataProofGridProps {
proofs: DataProof[];
accentColor?: string;
}
export function DataProofGrid({ proofs, accentColor = '#C41E3A' }: DataProofGridProps) {
return (
<div className="grid grid-cols-1 gap-6 sm:grid-cols-3">
{proofs.map((proof, index) => (
<motion.div
key={proof.metric}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: '-50px' }}
transition={{ duration: 0.5, delay: index * 0.1 }}
className="rounded-xl border border-gray-200 bg-white p-6 text-center shadow-sm"
>
<p
className="text-3xl font-bold tracking-tight"
style={{ color: accentColor }}
>
{proof.value}
</p>
<p className="mt-1 text-sm font-semibold text-gray-900">{proof.metric}</p>
<p className="mt-1 text-xs text-gray-500">{proof.description}</p>
</motion.div>
))}
</div>
);
}
+173
View File
@@ -0,0 +1,173 @@
'use client';
import { motion } from 'framer-motion';
import type { HeroTheme, HeroTexture } from '@/lib/constants/hero-themes';
interface DetailHeroProps {
theme: HeroTheme;
badge?: string;
title: string;
subtitle: string;
description?: string;
primaryAction?: { label: string; href: string };
secondaryAction?: { label: string; href: string };
}
function TextureOverlay({ texture }: { texture: HeroTexture }) {
if (texture.type === 'none') return null;
const textureStyles: Record<string, React.CSSProperties> = {
grid: {
backgroundImage:
'linear-gradient(rgba(255,255,255,.03) 1px, transparent 1px), linear-gradient(90deg, rgba(255,255,255,.03) 1px, transparent 1px)',
backgroundSize: '60px 60px',
},
'data-flow': {
backgroundImage:
'radial-gradient(circle at 20% 50%, rgba(255,255,255,.04) 0%, transparent 50%), radial-gradient(circle at 80% 20%, rgba(255,255,255,.03) 0%, transparent 40%)',
},
layers: {
backgroundImage:
'linear-gradient(180deg, transparent 0%, rgba(255,255,255,.02) 50%, transparent 100%)',
},
'shield-hex': {
backgroundImage:
'repeating-linear-gradient(60deg, transparent, transparent 30px, rgba(255,255,255,.025) 30px, rgba(255,255,255,.025) 31px)',
},
circuit: {
backgroundImage:
'linear-gradient(90deg, transparent 49%, rgba(255,255,255,.02) 49%, rgba(255,255,255,.02) 51%, transparent 51%)',
backgroundSize: '40px 40px',
},
};
return (
<div
className="pointer-events-none absolute inset-0"
style={{ opacity: texture.opacity, ...textureStyles[texture.type] }}
/>
);
}
export function DetailHero({
theme,
badge,
title,
subtitle,
description,
primaryAction,
secondaryAction,
}: DetailHeroProps) {
const isCenterLayout = theme.layout === 'center' || theme.layout === 'wide';
const isIconLeft = theme.layout === 'icon-left';
const containerClasses =
theme.layout === 'wide'
? 'container-wide py-24 lg:py-32'
: 'container-wide py-20 lg:py-28';
const contentClasses = [
'max-w-4xl',
isCenterLayout ? 'mx-auto text-center' : '',
isIconLeft ? 'flex items-center gap-8' : '',
]
.filter(Boolean)
.join(' ');
return (
<section
className="relative overflow-hidden"
style={{
background: `linear-gradient(${theme.gradientAngle}deg, ${theme.gradientFrom}, ${theme.gradientTo})`,
}}
>
<TextureOverlay texture={theme.texture} />
<div className={containerClasses}>
<motion.div
initial={{ opacity: 0, y: 24 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, ease: 'easeOut' }}
className={contentClasses}
>
{(badge || theme.badge) && (
<motion.span
initial={{ opacity: 0, scale: 0.9 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ delay: 0.1, duration: 0.4 }}
className={`mb-4 inline-block rounded-full px-3 py-1 text-xs font-medium tracking-wide ${
isCenterLayout ? '' : ''
}`}
style={{
backgroundColor: theme.accentBg,
color: theme.accentColor,
border: `1px solid ${theme.accentColor}33`,
}}
>
{badge || theme.badge}
</motion.span>
)}
<h1
className="text-3xl font-bold tracking-tight text-white sm:text-4xl lg:text-5xl"
style={{ lineHeight: 1.2 }}
>
{title}
</h1>
<p
className={`mt-4 text-lg ${
isCenterLayout ? 'mx-auto max-w-2xl' : 'max-w-2xl'
}`}
style={{ color: `${theme.accentColor}cc`, lineHeight: 1.6 }}
>
{subtitle}
</p>
{description && (
<p
className={`mt-3 text-base leading-relaxed ${
isCenterLayout ? 'mx-auto max-w-2xl' : 'max-w-2xl'
}`}
style={{ color: 'rgba(255,255,255,0.7)' }}
>
{description}
</p>
)}
{(primaryAction || secondaryAction) && (
<div
className={`mt-8 flex flex-wrap gap-4 ${
isCenterLayout ? 'justify-center' : ''
}`}
>
{primaryAction && (
<a
href={primaryAction.href}
className="inline-flex items-center rounded-lg px-6 py-3 text-sm font-semibold text-white shadow-lg transition-all hover:shadow-xl hover:brightness-110"
style={{
backgroundColor: theme.accentColor,
}}
>
{primaryAction.label}
</a>
)}
{secondaryAction && (
<a
href={secondaryAction.href}
className="inline-flex items-center rounded-lg border px-6 py-3 text-sm font-semibold transition-all hover:bg-white/10"
style={{
borderColor: `${theme.accentColor}66`,
color: `${theme.accentColor}ee`,
}}
>
{secondaryAction.label}
</a>
)}
</div>
)}
</motion.div>
</div>
</section>
);
}
@@ -0,0 +1,91 @@
'use client';
import { motion } from 'framer-motion';
import { CheckCircle2, Zap, Shield, TrendingUp, type LucideIcon } from 'lucide-react';
import type { Product } from '@/lib/constants/products';
interface ProductValueSectionProps {
product: Product;
}
const icons: LucideIcon[] = [CheckCircle2, Zap, Shield, TrendingUp];
export function ProductValueSection({ product }: ProductValueSectionProps) {
return (
<section className="bg-white py-16 lg:py-20">
<div className="container-wide">
<div className="grid grid-cols-1 gap-12 lg:grid-cols-2">
<motion.div
initial={{ opacity: 0, x: -20 }}
whileInView={{ opacity: 1, x: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.5 }}
>
<h2 className="mb-4 text-2xl font-bold text-gray-900 lg:text-3xl">
</h2>
<p className="mb-6 text-sm leading-relaxed text-gray-600">
{product.overview}
</p>
<ul className="space-y-3">
{product.features.map((feature, index) => {
const Icon = icons[index % icons.length]!;
return (
<motion.li
key={feature}
initial={{ opacity: 0, x: -10 }}
whileInView={{ opacity: 1, x: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.3, delay: index * 0.05 }}
className="flex items-start gap-3"
>
<Icon className="mt-0.5 h-5 w-5 shrink-0 text-[#C41E3A]" />
<span className="text-sm leading-relaxed text-gray-700">{feature}</span>
</motion.li>
);
})}
</ul>
</motion.div>
<motion.div
initial={{ opacity: 0, x: 20 }}
whileInView={{ opacity: 1, x: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.5, delay: 0.15 }}
>
<h2 className="mb-4 text-2xl font-bold text-gray-900 lg:text-3xl">
</h2>
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
{product.benefits.map((benefit, index) => (
<motion.div
key={benefit}
initial={{ opacity: 0, y: 10 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.3, delay: index * 0.06 }}
className="rounded-lg border border-gray-100 bg-gray-50 p-4"
>
<CheckCircle2 className="mb-2 h-5 w-5 text-green-600" />
<p className="text-sm leading-relaxed text-gray-700">{benefit}</p>
</motion.div>
))}
</div>
<div className="mt-8 rounded-xl bg-gray-900 p-6">
<h3 className="mb-3 text-sm font-semibold text-white"></h3>
<ul className="space-y-2">
{product.specs.slice(0, 4).map((spec) => (
<li key={spec} className="flex items-start gap-2 text-xs text-gray-300">
<span className="mt-1 h-1 w-1 shrink-0 rounded-full bg-[#C41E3A]" />
{spec}
</li>
))}
</ul>
</div>
</motion.div>
</div>
</div>
</section>
);
}
@@ -0,0 +1,128 @@
'use client';
import { motion } from 'framer-motion';
import {
CheckCircle2,
Clock,
Target,
Rocket,
ClipboardCheck,
Headphones,
type LucideIcon,
} from 'lucide-react';
import type { Service } from '@/lib/constants/services';
interface ServiceValueSectionProps {
service: Service;
}
const processIcons: LucideIcon[] = [Target, Rocket, ClipboardCheck, Headphones];
export function ServiceValueSection({ service }: ServiceValueSectionProps) {
return (
<section className="bg-white py-16 lg:py-20">
<div className="container-wide">
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.5 }}
className="mb-10"
>
<p className="text-sm leading-relaxed text-gray-600">{service.overview}</p>
</motion.div>
<div className="grid grid-cols-1 gap-12 lg:grid-cols-2">
<motion.div
initial={{ opacity: 0, x: -20 }}
whileInView={{ opacity: 1, x: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.5 }}
>
<h2 className="mb-6 text-xl font-bold text-gray-900"></h2>
<ul className="space-y-3">
{service.features.map((feature, index) => (
<motion.li
key={feature}
initial={{ opacity: 0, x: -10 }}
whileInView={{ opacity: 1, x: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.3, delay: index * 0.05 }}
className="flex items-start gap-3"
>
<CheckCircle2 className="mt-0.5 h-5 w-5 shrink-0 text-[#C41E3A]" />
<span className="text-sm leading-relaxed text-gray-700">{feature}</span>
</motion.li>
))}
</ul>
<h3 className="mb-4 mt-8 text-lg font-bold text-gray-900"></h3>
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
{service.benefits.map((benefit, index) => (
<motion.div
key={benefit}
initial={{ opacity: 0, y: 10 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.3, delay: index * 0.06 }}
className="rounded-lg border border-red-50 bg-red-50/30 p-3"
>
<p className="text-sm text-gray-700">{benefit}</p>
</motion.div>
))}
</div>
</motion.div>
<motion.div
initial={{ opacity: 0, x: 20 }}
whileInView={{ opacity: 1, x: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.5, delay: 0.15 }}
>
<h2 className="mb-6 flex items-center gap-2 text-xl font-bold text-gray-900">
<Clock className="h-5 w-5 text-[#C41E3A]" />
</h2>
<div className="relative">
{service.process.map((step, index) => {
const Icon = processIcons[index % processIcons.length]!;
const isLast = index === service.process.length - 1;
return (
<motion.div
key={step}
initial={{ opacity: 0, x: 20 }}
whileInView={{ opacity: 1, x: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.3, delay: index * 0.08 }}
className="relative flex gap-4 pb-6 last:pb-0"
>
{!isLast && (
<div className="absolute left-[19px] top-11 h-full w-px bg-gray-200" />
)}
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-full bg-[#C41E3A] text-white shadow-md">
<Icon className="h-5 w-5" />
</div>
<div className="pt-1">
<div className="mb-1 flex items-center gap-2">
<span className="text-xs font-semibold text-[#C41E3A]">
{index + 1}
</span>
</div>
<p className="text-sm font-medium text-gray-900">
{step.split('')[0]}
</p>
<p className="mt-0.5 text-xs leading-relaxed text-gray-500">
{step.split('')[1] || step.split(':')[1] || ''}
</p>
</div>
</motion.div>
);
})}
</div>
</motion.div>
</div>
</div>
</section>
);
}
@@ -0,0 +1,158 @@
'use client';
import { motion } from 'framer-motion';
import { AlertTriangle, CheckCircle2, ArrowRight, Package, Wrench } from 'lucide-react';
import type { Solution } from '@/lib/constants/solutions';
import { PRODUCTS } from '@/lib/constants/products';
import { SERVICES } from '@/lib/constants/services';
interface SolutionValueSectionProps {
solution: Solution;
}
export function SolutionValueSection({ solution }: SolutionValueSectionProps) {
const primaryProducts = solution.suiteCombination.primaryProducts
.map((id) => PRODUCTS.find((p) => p.id === id))
.filter(Boolean);
const complementaryServices = solution.suiteCombination.complementaryServices
.map((id) => SERVICES.find((s) => s.id === id))
.filter(Boolean);
return (
<section className="bg-white py-16 lg:py-20">
<div className="container-wide">
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.5 }}
className="mb-12 text-center"
>
<h2 className="text-2xl font-bold text-gray-900 lg:text-3xl">
{solution.valueProposition.headline}
</h2>
</motion.div>
<div className="mb-12 grid grid-cols-1 gap-6 md:grid-cols-3">
{solution.valueProposition.points.map((point, index) => (
<motion.div
key={point.title}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.4, delay: index * 0.1 }}
className="rounded-xl border border-gray-100 bg-gray-50/50 p-6 text-center"
>
<div className="mx-auto mb-4 flex h-12 w-12 items-center justify-center rounded-xl bg-[#C41E3A]/10 text-[#C41E3A]">
<span className="text-lg">{point.icon}</span>
</div>
<h3 className="mb-2 text-base font-semibold text-gray-900">{point.title}</h3>
<p className="text-sm leading-relaxed text-gray-600">{point.description}</p>
</motion.div>
))}
</div>
<div className="grid grid-cols-1 gap-8 lg:grid-cols-2">
<motion.div
initial={{ opacity: 0, x: -20 }}
whileInView={{ opacity: 1, x: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.5 }}
>
<h3 className="mb-4 flex items-center gap-2 text-lg font-bold text-gray-900">
<AlertTriangle className="h-5 w-5 text-orange-500" />
</h3>
<ul className="space-y-3">
{solution.challenges.map((challenge, index) => (
<motion.li
key={challenge}
initial={{ opacity: 0, x: -10 }}
whileInView={{ opacity: 1, x: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.3, delay: index * 0.05 }}
className="flex items-start gap-2 text-sm text-gray-700"
>
<span className="mt-1.5 h-1.5 w-1.5 shrink-0 rounded-full bg-orange-400" />
{challenge}
</motion.li>
))}
</ul>
</motion.div>
<motion.div
initial={{ opacity: 0, x: 20 }}
whileInView={{ opacity: 1, x: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.5, delay: 0.15 }}
>
<h3 className="mb-4 flex items-center gap-2 text-lg font-bold text-gray-900">
<CheckCircle2 className="h-5 w-5 text-green-500" />
</h3>
<ul className="space-y-3">
{solution.solutions.map((sol, index) => (
<motion.li
key={sol}
initial={{ opacity: 0, x: 10 }}
whileInView={{ opacity: 1, x: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.3, delay: index * 0.05 }}
className="flex items-start gap-2 text-sm text-gray-700"
>
<CheckCircle2 className="mt-0.5 h-4 w-4 shrink-0 text-green-500" />
{sol}
</motion.li>
))}
</ul>
</motion.div>
</div>
{(primaryProducts.length > 0 || complementaryServices.length > 0) && (
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.5, delay: 0.2 }}
className="mt-12 rounded-2xl bg-gradient-to-br from-gray-50 to-gray-100 p-8"
>
<h3 className="mb-2 text-lg font-bold text-gray-900"></h3>
<p className="mb-6 text-sm leading-relaxed text-gray-600">
{solution.suiteCombination.rationale}
</p>
<div className="flex flex-wrap items-center gap-4">
{primaryProducts.map((product) =>
product ? (
<a
key={product.id}
href={`/products/${product.id}`}
className="inline-flex items-center gap-2 rounded-lg border border-blue-200 bg-white px-4 py-2 text-sm font-medium text-blue-700 transition-all hover:bg-blue-50 hover:border-blue-300"
>
<Package className="h-4 w-4" />
{product.title}
<ArrowRight className="h-3 w-3" />
</a>
) : null,
)}
{complementaryServices.map((service) =>
service ? (
<a
key={service.id}
href={`/services/${service.id}`}
className="inline-flex items-center gap-2 rounded-lg border border-purple-200 bg-white px-4 py-2 text-sm font-medium text-purple-700 transition-all hover:bg-purple-50 hover:border-purple-300"
>
<Wrench className="h-4 w-4" />
{service.title}
<ArrowRight className="h-3 w-3" />
</a>
) : null,
)}
</div>
</motion.div>
)}
</div>
</section>
);
}
@@ -0,0 +1,71 @@
'use client';
import { CaseStudyCard } from './detail-case-study';
import { DataProofGrid } from './detail-data-proof';
import { CertificationList } from './detail-certification';
import type { CaseStudy, DataProof, Certification } from '@/lib/constants/products';
interface DetailTrustSectionProps {
caseStudies?: CaseStudy[];
dataProofs?: DataProof[];
certifications?: Certification[];
accentColor?: string;
}
export function DetailTrustSection({
caseStudies = [],
dataProofs = [],
certifications = [],
accentColor = '#C41E3A',
}: DetailTrustSectionProps) {
const hasContent = caseStudies.length > 0 || dataProofs.length > 0 || certifications.length > 0;
if (!hasContent) return null;
return (
<section className="bg-gray-50 py-16 lg:py-20">
<div className="container-wide">
{dataProofs.length > 0 && (
<div className="mb-12">
<h2 className="mb-2 text-center text-2xl font-bold text-gray-900">
</h2>
<p className="mx-auto mb-8 max-w-2xl text-center text-sm text-gray-500">
</p>
<DataProofGrid proofs={dataProofs} accentColor={accentColor} />
</div>
)}
{caseStudies.length > 0 && (
<div className="mb-12">
<h2 className="mb-2 text-center text-2xl font-bold text-gray-900">
</h2>
<p className="mx-auto mb-8 max-w-2xl text-center text-sm text-gray-500">
</p>
<div className="grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-3">
{caseStudies.map((study, index) => (
<CaseStudyCard key={study.client} study={study} index={index} />
))}
</div>
</div>
)}
{certifications.length > 0 && (
<div>
<h2 className="mb-2 text-center text-2xl font-bold text-gray-900">
</h2>
<p className="mx-auto mb-6 max-w-2xl text-center text-sm text-gray-500">
</p>
<div className="flex justify-center">
<CertificationList certifications={certifications} />
</div>
</div>
)}
</div>
</section>
);
}