refactor: P1 - remove unused legacy components
- Delete old homepage components - Delete unused UI components
This commit is contained in:
@@ -1,243 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import { motion } from 'framer-motion';
|
||||
import { StaticLink } from '@/components/ui/static-link';
|
||||
import { RippleButton, SealButton } from '@/components/ui/ripple-button';
|
||||
import { MagneticButton, BlurReveal, CounterWithEffect } from '@/lib/animations';
|
||||
import { COMPANY_INFO, STATS } from '@/lib/constants';
|
||||
import { ArrowRight, Shield, Zap, Award } from 'lucide-react';
|
||||
import { useReducedMotion } from '@/hooks/use-reduced-motion';
|
||||
import { trackButtonClick, trackServiceInterest } from '@/lib/analytics';
|
||||
|
||||
interface HeroContentProps {
|
||||
isVisible: boolean;
|
||||
}
|
||||
|
||||
const features = [
|
||||
{ icon: Shield, text: '安全可靠' },
|
||||
{ icon: Zap, text: '高效便捷' },
|
||||
{ icon: Award, text: '专业服务' },
|
||||
];
|
||||
|
||||
function scrollTo(id: string) {
|
||||
const element = document.getElementById(id);
|
||||
if (element) {
|
||||
element.scrollIntoView({ behavior: 'smooth' });
|
||||
}
|
||||
}
|
||||
|
||||
function handleKeyDown(event: React.KeyboardEvent<HTMLButtonElement>, id: string) {
|
||||
if (event.key === 'Enter' || event.key === ' ') {
|
||||
event.preventDefault();
|
||||
scrollTo(id);
|
||||
}
|
||||
}
|
||||
|
||||
export function HeroContent({ isVisible }: HeroContentProps) {
|
||||
const shouldReduceMotion = useReducedMotion();
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
initial={shouldReduceMotion ? {} : { opacity: 0, y: 20, scale: 0.95 }}
|
||||
animate={isVisible ? { opacity: 1, y: 0, scale: 1 } : {}}
|
||||
transition={shouldReduceMotion ? { duration: 0 } : { duration: 0.6, ease: [0.16, 1, 0.3, 1] }}
|
||||
className="mb-8"
|
||||
>
|
||||
<span className="inline-flex items-center gap-2 px-5 py-2.5 rounded-full border border-[#1C1C1C]/20 bg-[#F5F5F5] text-[#1C1C1C] text-sm font-medium">
|
||||
智连未来,成长伙伴
|
||||
</span>
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
|
||||
export function HeroTitle({ isVisible }: HeroContentProps) {
|
||||
const shouldReduceMotion = useReducedMotion();
|
||||
|
||||
return (
|
||||
<motion.h1
|
||||
id="hero-heading"
|
||||
initial={shouldReduceMotion ? {} : { opacity: 0, y: 20 }}
|
||||
animate={isVisible ? { opacity: 1, y: 0 } : {}}
|
||||
transition={shouldReduceMotion ? { duration: 0 } : { duration: 0.6, delay: 0.1 }}
|
||||
className="text-5xl sm:text-6xl lg:text-7xl tracking-tight mb-6 font-brand"
|
||||
style={{
|
||||
fontWeight: 'normal',
|
||||
WebkitFontSmoothing: 'antialiased',
|
||||
MozOsxFontSmoothing: 'grayscale',
|
||||
textRendering: 'optimizeLegibility'
|
||||
}}
|
||||
>
|
||||
{COMPANY_INFO.shortName}
|
||||
</motion.h1>
|
||||
);
|
||||
}
|
||||
|
||||
export function HeroDescription(_props: HeroContentProps) {
|
||||
return (
|
||||
<div className="mb-10">
|
||||
<BlurReveal delay={0.3}>
|
||||
<p className="text-xl sm:text-2xl text-[#4A5568] mb-4">
|
||||
<span className="font-semibold bg-gradient-to-r from-[#C41E3A] via-[#E04A68] to-[#C41E3A] bg-clip-text text-transparent">
|
||||
企业数字化转型服务商
|
||||
</span>
|
||||
</p>
|
||||
</BlurReveal>
|
||||
<BlurReveal delay={0.4}>
|
||||
<p className="text-lg text-[#718096] max-w-2xl mx-auto leading-relaxed">
|
||||
以智慧连接数字趋势,以伙伴身份陪您成长——您的数字化转型同行者
|
||||
</p>
|
||||
</BlurReveal>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function HeroButtons({ isVisible }: HeroContentProps) {
|
||||
const shouldReduceMotion = useReducedMotion();
|
||||
|
||||
const handleConsultClick = () => {
|
||||
trackButtonClick('consult_now', 'hero_section');
|
||||
trackServiceInterest('consultation');
|
||||
};
|
||||
|
||||
const handleLearnMoreClick = () => {
|
||||
trackButtonClick('learn_more', 'hero_section');
|
||||
scrollTo('about');
|
||||
};
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
initial={shouldReduceMotion ? {} : { opacity: 0, y: 20 }}
|
||||
animate={isVisible ? { opacity: 1, y: 0 } : {}}
|
||||
transition={shouldReduceMotion ? { duration: 0 } : { duration: 0.6, delay: 0.3 }}
|
||||
className="flex flex-col sm:flex-row items-center justify-center gap-4 mb-8"
|
||||
>
|
||||
<MagneticButton strength={0.4}>
|
||||
<StaticLink href="/contact" onClick={handleConsultClick}>
|
||||
<SealButton size="lg" className="min-w-45">
|
||||
立即咨询
|
||||
<ArrowRight className="w-4 h-4 ml-2" />
|
||||
</SealButton>
|
||||
</StaticLink>
|
||||
</MagneticButton>
|
||||
<MagneticButton strength={0.4}>
|
||||
<RippleButton
|
||||
size="lg"
|
||||
variant="outline"
|
||||
onClick={handleLearnMoreClick}
|
||||
onKeyDown={(e) => handleKeyDown(e, 'about')}
|
||||
className="min-w-45"
|
||||
>
|
||||
了解更多
|
||||
</RippleButton>
|
||||
</MagneticButton>
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
|
||||
export function HeroFeatures({ isVisible }: HeroContentProps) {
|
||||
const shouldReduceMotion = useReducedMotion();
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
initial={shouldReduceMotion ? {} : { opacity: 0, y: 20 }}
|
||||
animate={isVisible ? { opacity: 1, y: 0 } : {}}
|
||||
transition={shouldReduceMotion ? { duration: 0 } : { duration: 0.6, delay: 0.35 }}
|
||||
className="flex flex-wrap gap-4 justify-center mb-16"
|
||||
>
|
||||
{features.map((feature, index) => (
|
||||
<motion.div
|
||||
key={index}
|
||||
initial={shouldReduceMotion ? {} : { opacity: 0, scale: 0.9 }}
|
||||
animate={isVisible ? { opacity: 1, scale: 1 } : {}}
|
||||
transition={shouldReduceMotion ? { duration: 0 } : { duration: 0.4, delay: 0.4 + index * 0.1 }}
|
||||
whileHover={shouldReduceMotion ? {} : { scale: 1.05, y: -2 }}
|
||||
className="flex items-center gap-2 px-4 py-2 rounded-full bg-[#FAFAFA] border border-[#E5E5E5] transition-all duration-300 hover:border-[#1C1C1C] hover:shadow-md cursor-default"
|
||||
>
|
||||
<feature.icon className="w-4 h-4 text-[#C41E3A]" />
|
||||
<span className="text-sm text-[#3D3D3D]">{feature.text}</span>
|
||||
</motion.div>
|
||||
))}
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
|
||||
export function HeroStats() {
|
||||
const [statsVisible, setStatsVisible] = useState(true);
|
||||
const shouldReduceMotion = useReducedMotion();
|
||||
|
||||
useEffect(() => {
|
||||
const statsEl = document.getElementById('stats-section');
|
||||
if (!statsEl) {return;}
|
||||
|
||||
const observer = new IntersectionObserver(
|
||||
([entry]) => {
|
||||
if (entry?.isIntersecting) {
|
||||
setStatsVisible(true);
|
||||
}
|
||||
},
|
||||
{ threshold: 0.5 }
|
||||
);
|
||||
|
||||
observer.observe(statsEl);
|
||||
return () => observer.disconnect();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
id="stats-section"
|
||||
initial={shouldReduceMotion ? {} : { opacity: 0, y: 20 }}
|
||||
animate={statsVisible ? { opacity: 1, y: 0 } : {}}
|
||||
transition={shouldReduceMotion ? { duration: 0 } : { duration: 0.6, delay: 0.4 }}
|
||||
className="pt-16 border-t border-[#E2E8F0]"
|
||||
>
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-8 md:gap-12">
|
||||
{STATS.map((stat, index) => (
|
||||
<HeroStatItem
|
||||
key={stat.label}
|
||||
stat={stat}
|
||||
index={index}
|
||||
shouldAnimate={statsVisible}
|
||||
shouldReduceMotion={shouldReduceMotion}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
|
||||
function HeroStatItem({ stat, index, shouldAnimate, shouldReduceMotion }: {
|
||||
stat: { value: string; label: string };
|
||||
index: number;
|
||||
shouldAnimate: boolean;
|
||||
shouldReduceMotion: boolean;
|
||||
}) {
|
||||
const numericValue = parseInt(stat.value.replace(/\D/g, ''));
|
||||
const suffix = stat.value.replace(/[\d]/g, '');
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
className="group cursor-default text-center"
|
||||
initial={shouldReduceMotion ? {} : { opacity: 0, y: 20, scale: 0.9 }}
|
||||
animate={shouldAnimate ? { opacity: 1, y: 0, scale: 1 } : {}}
|
||||
transition={shouldReduceMotion ? { duration: 0 } : { duration: 0.5, delay: index * 0.1, type: 'spring', stiffness: 100 }}
|
||||
whileHover={shouldReduceMotion ? {} : { scale: 1.05, y: -5 }}
|
||||
>
|
||||
<div className="text-4xl sm:text-5xl font-bold text-[#C41E3A] mb-3">
|
||||
{shouldAnimate ? (
|
||||
<CounterWithEffect
|
||||
end={numericValue}
|
||||
suffix={suffix}
|
||||
effect="bounce"
|
||||
duration={2000}
|
||||
/>
|
||||
) : (
|
||||
<span className="text-[#CBD5E0]">0{suffix}</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="text-sm text-[#718096] group-hover:text-[#4A5568] transition-colors">
|
||||
{stat.label}
|
||||
</div>
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
@@ -1,166 +0,0 @@
|
||||
import { describe, it, expect, jest, beforeAll } from '@jest/globals';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import '@testing-library/jest-dom';
|
||||
|
||||
jest.mock('framer-motion', () => ({
|
||||
motion: {
|
||||
div: ({ children, className, ...props }: { children: React.ReactNode; className?: string }) => (
|
||||
<div className={className} {...props}>
|
||||
{children}
|
||||
</div>
|
||||
),
|
||||
section: ({ children, className, ...props }: { children: React.ReactNode; className?: string }) => (
|
||||
<section className={className} {...props}>
|
||||
{children}
|
||||
</section>
|
||||
),
|
||||
span: ({ children, className, ...props }: { children: React.ReactNode; className?: string }) => (
|
||||
<span className={className} {...props}>
|
||||
{children}
|
||||
</span>
|
||||
),
|
||||
h1: ({ children, className, ...props }: { children: React.ReactNode; className?: string }) => (
|
||||
<h1 className={className} {...props}>
|
||||
{children}
|
||||
</h1>
|
||||
),
|
||||
},
|
||||
AnimatePresence: ({ children }: { children: React.ReactNode }) => <>{children}</>,
|
||||
}));
|
||||
|
||||
jest.mock('next/link', () => {
|
||||
const MockLink = ({ children, href, ...props }: { children: React.ReactNode; href: string }) => (
|
||||
<a href={href} {...props}>
|
||||
{children}
|
||||
</a>
|
||||
);
|
||||
MockLink.displayName = 'MockLink';
|
||||
return MockLink;
|
||||
});
|
||||
|
||||
jest.mock('lucide-react', () => ({
|
||||
ArrowRight: () => <span data-testid="arrow-right" />,
|
||||
Shield: () => <span data-testid="shield-icon" />,
|
||||
Zap: () => <span data-testid="zap-icon" />,
|
||||
Award: () => <span data-testid="award-icon" />,
|
||||
}));
|
||||
|
||||
jest.mock('next/dynamic', () => ({
|
||||
__esModule: true,
|
||||
default: () => {
|
||||
const MockDynamic = () => null;
|
||||
MockDynamic.displayName = 'MockDynamic';
|
||||
return MockDynamic;
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock('@/components/ui/ripple-button', () => ({
|
||||
RippleButton: ({ children, className, ...props }: { children: React.ReactNode; className?: string }) => (
|
||||
<button className={className} {...props}>
|
||||
{children}
|
||||
</button>
|
||||
),
|
||||
SealButton: ({ children, className, ...props }: { children: React.ReactNode; className?: string }) => (
|
||||
<button className={className} {...props}>
|
||||
{children}
|
||||
</button>
|
||||
),
|
||||
}));
|
||||
|
||||
jest.mock('@/lib/animations', () => ({
|
||||
GradientText: ({ children, className }: { children: React.ReactNode; className?: string }) => (
|
||||
<span className={className}>{children}</span>
|
||||
),
|
||||
MagneticButton: ({ children, className }: { children: React.ReactNode; className?: string }) => (
|
||||
<button className={className}>{children}</button>
|
||||
),
|
||||
BlurReveal: ({ children, className }: { children: React.ReactNode; className?: string }) => (
|
||||
<div className={className}>{children}</div>
|
||||
),
|
||||
CounterWithEffect: ({ end, suffix, className }: { end: number; suffix?: string; className?: string }) => (
|
||||
<span className={className}>{end}{suffix || ''}</span>
|
||||
),
|
||||
}));
|
||||
|
||||
jest.mock('@/lib/constants', () => ({
|
||||
COMPANY_INFO: {
|
||||
name: '四川睿新致远科技有限公司',
|
||||
shortName: '睿新致遠',
|
||||
description: '以智慧连接数字趋势,以伙伴身份陪您成长',
|
||||
},
|
||||
STATS: [
|
||||
{ value: '10+', label: '企业客户' },
|
||||
{ value: '20+', label: '成功案例' },
|
||||
{ value: '30+', label: '项目交付' },
|
||||
{ value: '12+', label: '年团队经验' },
|
||||
],
|
||||
}));
|
||||
|
||||
jest.mock('./hero-section-atoms', () => ({
|
||||
HeroContent: () => <div>智连未来,成长伙伴</div>,
|
||||
HeroTitle: () => <h1>睿新致遠</h1>,
|
||||
HeroDescription: () => <p>企业数字化转型服务商</p>,
|
||||
HeroButtons: () => <div><button>立即咨询</button><button>了解更多</button></div>,
|
||||
HeroFeatures: () => <div><span>安全可靠</span><span>高效便捷</span><span>专业服务</span></div>,
|
||||
HeroStats: () => (
|
||||
<div data-testid="hero-stats">
|
||||
<span>企业客户</span>
|
||||
<span>成功案例</span>
|
||||
<span>项目交付</span>
|
||||
<span>年团队经验</span>
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
|
||||
import { HeroSection } from './hero-section';
|
||||
import { HeroStats } from './hero-section-atoms';
|
||||
|
||||
describe('HeroSection', () => {
|
||||
beforeAll(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('Rendering', () => {
|
||||
it('should render hero section', () => {
|
||||
render(<HeroSection heroStats={<HeroStats />} />);
|
||||
const section = document.querySelector('section#home');
|
||||
expect(section).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render company name', () => {
|
||||
render(<HeroSection heroStats={<HeroStats />} />);
|
||||
expect(screen.getByText('睿新致遠')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render features', () => {
|
||||
render(<HeroSection heroStats={<HeroStats />} />);
|
||||
expect(screen.getByText('安全可靠')).toBeInTheDocument();
|
||||
expect(screen.getByText('高效便捷')).toBeInTheDocument();
|
||||
expect(screen.getByText('专业服务')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Statistics', () => {
|
||||
it('should render statistics section', () => {
|
||||
render(<HeroSection heroStats={<HeroStats />} />);
|
||||
expect(screen.getByText('企业客户')).toBeInTheDocument();
|
||||
expect(screen.getByText('成功案例')).toBeInTheDocument();
|
||||
expect(screen.getByText('项目交付')).toBeInTheDocument();
|
||||
expect(screen.getByText('年团队经验')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Accessibility', () => {
|
||||
it('should have proper ARIA labels', () => {
|
||||
render(<HeroSection heroStats={<HeroStats />} />);
|
||||
const section = document.querySelector('section#home');
|
||||
expect(section).toHaveAttribute('aria-labelledby', 'hero-heading');
|
||||
});
|
||||
|
||||
it('should have accessible buttons', () => {
|
||||
render(<HeroSection heroStats={<HeroStats />} />);
|
||||
const buttons = screen.getAllByRole('button');
|
||||
expect(buttons.length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,73 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import dynamic from 'next/dynamic';
|
||||
import { HeroContent, HeroTitle, HeroDescription, HeroButtons, HeroFeatures } from './hero-section-atoms';
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
const InkBackground = dynamic(
|
||||
() => import('@/components/ui/ink-decoration').then(mod => ({ default: mod.InkBackground })),
|
||||
{ ssr: false }
|
||||
);
|
||||
|
||||
const DataParticleFlow = dynamic(
|
||||
() => import('@/components/effects/data-particle-flow').then(mod => ({ default: mod.DataParticleFlow })),
|
||||
{ ssr: false }
|
||||
);
|
||||
|
||||
const SubtleDots = dynamic(
|
||||
() => import('@/components/effects/subtle-dots').then(mod => ({ default: mod.SubtleDots })),
|
||||
{ ssr: false }
|
||||
);
|
||||
|
||||
export function HeroSection({ heroStats }: { heroStats: ReactNode }) {
|
||||
const [isVisible, setIsVisible] = useState(false);
|
||||
const sectionRef = useRef<HTMLElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const observer = new IntersectionObserver(
|
||||
([entry]) => {
|
||||
if (entry?.isIntersecting) {
|
||||
setIsVisible(true);
|
||||
}
|
||||
},
|
||||
{ threshold: 0.1 }
|
||||
);
|
||||
|
||||
if (sectionRef.current) {
|
||||
observer.observe(sectionRef.current);
|
||||
}
|
||||
|
||||
return () => observer.disconnect();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<section
|
||||
id="home"
|
||||
ref={sectionRef}
|
||||
aria-labelledby="hero-heading"
|
||||
className="relative min-h-screen flex items-center overflow-hidden bg-linear-to-b from-[#FAFAFA] to-white"
|
||||
>
|
||||
<InkBackground />
|
||||
<DataParticleFlow
|
||||
particleCount={60}
|
||||
color="#C41E3A"
|
||||
intensity="subtle"
|
||||
shape="square"
|
||||
effect="pulse"
|
||||
/>
|
||||
<SubtleDots color="#C41E3A" count={8} />
|
||||
|
||||
<div className="container-wide py-24 md:py-32 lg:py-40 relative z-10">
|
||||
<div className="max-w-4xl mx-auto text-center">
|
||||
<HeroContent isVisible={isVisible} />
|
||||
<HeroTitle isVisible={isVisible} />
|
||||
<HeroDescription isVisible={isVisible} />
|
||||
<HeroButtons isVisible={isVisible} />
|
||||
<HeroFeatures isVisible={isVisible} />
|
||||
{heroStats}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
import { STATS } from '@/lib/constants';
|
||||
|
||||
export function HeroStatsSSR() {
|
||||
return (
|
||||
<div className="pt-16 border-t border-[#E2E8F0]">
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-8 md:gap-12">
|
||||
{STATS.map((stat) => (
|
||||
<div
|
||||
key={stat.label}
|
||||
className="group cursor-default text-center"
|
||||
>
|
||||
<div className="text-4xl sm:text-5xl font-bold text-[#C41E3A] mb-3">
|
||||
{stat.value}
|
||||
</div>
|
||||
<div className="text-sm text-[#718096] group-hover:text-[#4A5568] transition-colors">
|
||||
{stat.label}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user