- 拆分品牌色「文字/底色」双通道:新增 --color-brand-ink(-rgb)(深色 #F87171) 与 tailwind brand.ink;text-brand → text-brand-ink 迁移 247 处; bg-brand/border-brand 保持 --color-brand-rgb 不翻转(红底白字 5.58:1 不能动) - dark 块补齐 --color-brand-bg: #2A1418(修复深色白字压浅粉底 1.04:1) - hero 徽章新增 --badge-accent-text 桥接:accentColor 直用作深底文字不可读 (#1e3a5f 深底 1.65:1),浅色取原色 / 深色 color-mix 提亮 - 硬编码颜色 token 化:bg-white/text-gray-*/border-gray-* → bg-bg-*/text-text-*/border-border-* - 可访问性:触控目标 <24px 归零(footer 链接列 [&_a]:min-h-6 收口 377 处等)、 news 搜索框补 aria-label、标题跳级 h2→h4 修正为 h3、装饰 blur 光斑容器补 overflow-hidden 消除伪文本裁剪 - 验证:tsc 0 / eslint 0 error / jest 0 失败用例 / dogfood 33 路由 × 双主题 P1-P3 全零
156 lines
5.6 KiB
TypeScript
156 lines
5.6 KiB
TypeScript
'use client';
|
|
|
|
import { motion } from 'framer-motion';
|
|
import { ScrollReveal } from '@/components/ui/scroll-reveal';
|
|
import { SectionLabel, EASE_OUT } from '@/components/ui/page-decoration';
|
|
import { Quote } from 'lucide-react';
|
|
import { useState } from 'react';
|
|
import { cn } from '@/lib/utils';
|
|
|
|
export interface Testimonial {
|
|
quote: string;
|
|
author: string;
|
|
title: string;
|
|
company: string;
|
|
industry: string;
|
|
}
|
|
|
|
export interface TestimonialSectionProps {
|
|
title: string;
|
|
subtitle?: string;
|
|
testimonials: Testimonial[];
|
|
accentColor?: string;
|
|
}
|
|
|
|
export function TestimonialSection({
|
|
title,
|
|
subtitle,
|
|
testimonials,
|
|
accentColor = '#C41E3A',
|
|
}: TestimonialSectionProps) {
|
|
const [activeIndex, setActiveIndex] = useState(0);
|
|
|
|
return (
|
|
<section className="relative py-20 sm:py-28 md:py-32 lg:py-40 overflow-hidden bg-bg-secondary">
|
|
<div className="max-w-container mx-auto px-4 sm:px-6 lg:px-10">
|
|
<ScrollReveal className="mb-16 sm:mb-20 max-w-3xl">
|
|
<SectionLabel>Client Stories</SectionLabel>
|
|
<h2 className="text-3xl md:text-4xl lg:text-5xl font-bold text-ink tracking-tight leading-tight mb-6">
|
|
{title}
|
|
</h2>
|
|
{subtitle && (
|
|
<p className="text-text-secondary leading-relaxed text-lg">
|
|
{subtitle}
|
|
</p>
|
|
)}
|
|
</ScrollReveal>
|
|
|
|
<div className="relative">
|
|
<div className="relative overflow-hidden">
|
|
<motion.div
|
|
className="flex"
|
|
animate={{ x: `-${activeIndex * 100}%` }}
|
|
transition={{ duration: 0.3, ease: EASE_OUT }}
|
|
>
|
|
{testimonials.map((testimonial, index) => (
|
|
<div key={index} className="w-full shrink-0">
|
|
<div className="max-w-4xl mx-auto">
|
|
<div className="relative p-10 lg:p-16 border border-border-primary bg-bg-primary">
|
|
<Quote className="absolute top-8 left-8 w-12 h-12 opacity-10 text-brand-ink" />
|
|
<div className="relative z-10">
|
|
<p className="text-xl lg:text-2xl text-text-primary leading-relaxed font-light mb-10 lg:mb-14">
|
|
「{testimonial.quote}」
|
|
</p>
|
|
<div className="flex items-center gap-5">
|
|
<div
|
|
className="w-14 h-14 rounded-full flex items-center justify-center text-white font-bold text-lg"
|
|
style={{ backgroundColor: accentColor }}
|
|
>
|
|
{testimonial.author.charAt(0)}
|
|
</div>
|
|
<div>
|
|
<div className="text-text-primary font-semibold text-lg">
|
|
{testimonial.author}
|
|
</div>
|
|
<div className="text-text-secondary text-sm">
|
|
{testimonial.title} · {testimonial.company}
|
|
</div>
|
|
<div className="text-text-muted text-xs mt-0.5">
|
|
{testimonial.industry}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</motion.div>
|
|
</div>
|
|
|
|
<div className="flex justify-center gap-3 mt-10">
|
|
{testimonials.map((_, index) => (
|
|
<button
|
|
key={index}
|
|
onClick={() => setActiveIndex(index)}
|
|
className={cn(
|
|
'h-2 rounded-full transition-all duration-300',
|
|
activeIndex === index
|
|
? 'w-8'
|
|
: 'w-2 bg-border-primary hover:bg-border-secondary'
|
|
)}
|
|
style={activeIndex === index ? { backgroundColor: accentColor } : {}}
|
|
/>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
);
|
|
}
|
|
|
|
export interface LogoWallProps {
|
|
title: string;
|
|
logos: { name: string; industry?: string }[];
|
|
}
|
|
|
|
export function LogoWall({ title, logos }: LogoWallProps) {
|
|
return (
|
|
<section className="relative py-16 sm:py-20 overflow-hidden bg-bg-primary">
|
|
<div className="max-w-container mx-auto px-4 sm:px-6 lg:px-10">
|
|
<ScrollReveal>
|
|
<div className="text-center mb-12">
|
|
<p className="text-sm text-text-muted tracking-[0.2em] uppercase font-medium">
|
|
{title}
|
|
</p>
|
|
</div>
|
|
</ScrollReveal>
|
|
|
|
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-6 gap-px bg-border-primary">
|
|
{logos.map((logo, index) => (
|
|
<motion.div
|
|
key={index}
|
|
initial={{ opacity: 0, y: 20 }}
|
|
whileInView={{ opacity: 1, y: 0 }}
|
|
viewport={{ once: true }}
|
|
transition={{ duration: 0.3, delay: index * 0.05, ease: EASE_OUT }}
|
|
className="flex items-center justify-center p-8 bg-bg-primary hover:bg-bg-secondary transition-colors duration-300"
|
|
>
|
|
<div className="text-center">
|
|
<div className="text-text-secondary font-semibold text-base hover:text-text-primary transition-colors duration-300">
|
|
{logo.name}
|
|
</div>
|
|
{logo.industry && (
|
|
<div className="text-text-muted text-xs mt-1">
|
|
{logo.industry}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</motion.div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</section>
|
|
);
|
|
}
|