diff --git a/src/app/(marketing)/home-content.tsx b/src/app/(marketing)/home-content.tsx deleted file mode 100644 index da64f7f..0000000 --- a/src/app/(marketing)/home-content.tsx +++ /dev/null @@ -1,122 +0,0 @@ -"use client"; - -import { useEffect } from 'react'; -import { useSearchParams } from 'next/navigation'; -import dynamic from 'next/dynamic'; -import { HeroSection } from "@/components/sections/hero-section"; -import { SectionSkeleton } from "@/components/ui/loading-skeleton"; -import type { ReactNode } from 'react'; - -declare global { - interface Window { - __isProgrammaticScroll?: boolean; - } -} - -const ServicesSection = dynamic( - () => import('@/components/sections/services-section').then(mod => ({ default: mod.ServicesSection })), - { - loading: () => , - ssr: false - } -); - -const HomeSolutionsSection = dynamic( - () => import('@/components/sections/home-solutions-section').then(mod => ({ default: mod.HomeSolutionsSection })), - { - loading: () => , - ssr: false - } -); - -const ProductsSection = dynamic( - () => import('@/components/sections/products-section').then(mod => ({ default: mod.ProductsSection })), - { - loading: () => , - ssr: false - } -); - -const CasesSection = dynamic( - () => import('@/components/sections/cases-section').then(mod => ({ default: mod.CasesSection })), - { - loading: () => , - ssr: false - } -); - -const AboutSection = dynamic( - () => import('@/components/sections/about-section').then(mod => ({ default: mod.AboutSection })), - { - loading: () => , - ssr: false - } -); - -const TeamSection = dynamic( - () => import('@/components/sections/team-section').then(mod => ({ default: mod.TeamSection })), - { - loading: () => , - ssr: false - } -); - -const NewsSection = dynamic( - () => import('@/components/sections/news-section').then(mod => ({ default: mod.NewsSection })), - { - loading: () => , - ssr: false - } -); - -function HomeContent({ heroStats }: { heroStats: ReactNode }) { - const searchParams = useSearchParams(); - - useEffect(() => { - const section = searchParams.get('section'); - if (!section) {return;} - - const maxAttempts = 50; - const interval = 100; - let attempts = 0; - - const scrollToSection = () => { - const targetElement = document.getElementById(section); - if (targetElement) { - window.__isProgrammaticScroll = true; - targetElement.scrollIntoView({ behavior: 'smooth', block: 'start' }); - setTimeout(() => { - window.__isProgrammaticScroll = false; - }, 2000); - return true; - } - return false; - }; - - if (scrollToSection()) {return;} - - const timer = setInterval(() => { - attempts++; - if (scrollToSection() || attempts >= maxAttempts) { - clearInterval(timer); - } - }, interval); - - return () => clearInterval(timer); - }, [searchParams]); - - return ( -
- - - - - - - - -
- ); -} - -export { HomeContent }; diff --git a/src/components/sections/hero-section-atoms.tsx b/src/components/sections/hero-section-atoms.tsx deleted file mode 100644 index f2d97fe..0000000 --- a/src/components/sections/hero-section-atoms.tsx +++ /dev/null @@ -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, id: string) { - if (event.key === 'Enter' || event.key === ' ') { - event.preventDefault(); - scrollTo(id); - } -} - -export function HeroContent({ isVisible }: HeroContentProps) { - const shouldReduceMotion = useReducedMotion(); - - return ( - - - 智连未来,成长伙伴 - - - ); -} - -export function HeroTitle({ isVisible }: HeroContentProps) { - const shouldReduceMotion = useReducedMotion(); - - return ( - - {COMPANY_INFO.shortName} - - ); -} - -export function HeroDescription(_props: HeroContentProps) { - return ( -
- -

- - 企业数字化转型服务商 - -

-
- -

- 以智慧连接数字趋势,以伙伴身份陪您成长——您的数字化转型同行者 -

-
-
- ); -} - -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 ( - - - - - 立即咨询 - - - - - - handleKeyDown(e, 'about')} - className="min-w-45" - > - 了解更多 - - - - ); -} - -export function HeroFeatures({ isVisible }: HeroContentProps) { - const shouldReduceMotion = useReducedMotion(); - - return ( - - {features.map((feature, index) => ( - - - {feature.text} - - ))} - - ); -} - -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 ( - -
- {STATS.map((stat, index) => ( - - ))} -
-
- ); -} - -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 ( - -
- {shouldAnimate ? ( - - ) : ( - 0{suffix} - )} -
-
- {stat.label} -
-
- ); -} diff --git a/src/components/sections/hero-section.test.tsx b/src/components/sections/hero-section.test.tsx deleted file mode 100644 index 37103bc..0000000 --- a/src/components/sections/hero-section.test.tsx +++ /dev/null @@ -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 }) => ( -
- {children} -
- ), - section: ({ children, className, ...props }: { children: React.ReactNode; className?: string }) => ( -
- {children} -
- ), - span: ({ children, className, ...props }: { children: React.ReactNode; className?: string }) => ( - - {children} - - ), - h1: ({ children, className, ...props }: { children: React.ReactNode; className?: string }) => ( -

- {children} -

- ), - }, - AnimatePresence: ({ children }: { children: React.ReactNode }) => <>{children}, -})); - -jest.mock('next/link', () => { - const MockLink = ({ children, href, ...props }: { children: React.ReactNode; href: string }) => ( - - {children} - - ); - MockLink.displayName = 'MockLink'; - return MockLink; -}); - -jest.mock('lucide-react', () => ({ - ArrowRight: () => , - Shield: () => , - Zap: () => , - Award: () => , -})); - -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 }) => ( - - ), - SealButton: ({ children, className, ...props }: { children: React.ReactNode; className?: string }) => ( - - ), -})); - -jest.mock('@/lib/animations', () => ({ - GradientText: ({ children, className }: { children: React.ReactNode; className?: string }) => ( - {children} - ), - MagneticButton: ({ children, className }: { children: React.ReactNode; className?: string }) => ( - - ), - BlurReveal: ({ children, className }: { children: React.ReactNode; className?: string }) => ( -
{children}
- ), - CounterWithEffect: ({ end, suffix, className }: { end: number; suffix?: string; className?: string }) => ( - {end}{suffix || ''} - ), -})); - -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: () =>
智连未来,成长伙伴
, - HeroTitle: () =>

睿新致遠

, - HeroDescription: () =>

企业数字化转型服务商

, - HeroButtons: () =>
, - HeroFeatures: () =>
安全可靠高效便捷专业服务
, - HeroStats: () => ( -
- 企业客户 - 成功案例 - 项目交付 - 年团队经验 -
- ), -})); - -import { HeroSection } from './hero-section'; -import { HeroStats } from './hero-section-atoms'; - -describe('HeroSection', () => { - beforeAll(() => { - jest.clearAllMocks(); - }); - - describe('Rendering', () => { - it('should render hero section', () => { - render(} />); - const section = document.querySelector('section#home'); - expect(section).toBeInTheDocument(); - }); - - it('should render company name', () => { - render(} />); - expect(screen.getByText('睿新致遠')).toBeInTheDocument(); - }); - - it('should render features', () => { - render(} />); - expect(screen.getByText('安全可靠')).toBeInTheDocument(); - expect(screen.getByText('高效便捷')).toBeInTheDocument(); - expect(screen.getByText('专业服务')).toBeInTheDocument(); - }); - }); - - describe('Statistics', () => { - it('should render statistics section', () => { - render(} />); - 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(} />); - const section = document.querySelector('section#home'); - expect(section).toHaveAttribute('aria-labelledby', 'hero-heading'); - }); - - it('should have accessible buttons', () => { - render(} />); - const buttons = screen.getAllByRole('button'); - expect(buttons.length).toBeGreaterThan(0); - }); - }); -}); diff --git a/src/components/sections/hero-section.tsx b/src/components/sections/hero-section.tsx deleted file mode 100644 index 79a7d95..0000000 --- a/src/components/sections/hero-section.tsx +++ /dev/null @@ -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(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 ( -
- - - - -
-
- - - - - - {heroStats} -
-
-
- ); -} diff --git a/src/components/sections/hero-stats-ssr.tsx b/src/components/sections/hero-stats-ssr.tsx deleted file mode 100644 index bd0dff4..0000000 --- a/src/components/sections/hero-stats-ssr.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import { STATS } from '@/lib/constants'; - -export function HeroStatsSSR() { - return ( -
-
- {STATS.map((stat) => ( -
-
- {stat.value} -
-
- {stat.label} -
-
- ))} -
-
- ); -} diff --git a/src/components/ui/animated-card.test.tsx b/src/components/ui/animated-card.test.tsx deleted file mode 100644 index 4d96a24..0000000 --- a/src/components/ui/animated-card.test.tsx +++ /dev/null @@ -1,185 +0,0 @@ -import { describe, it, expect, beforeEach, jest } from '@jest/globals'; -import { render, screen, fireEvent } from '@testing-library/react'; -import '@testing-library/jest-dom'; -import { InkCard, GeometricCard, FlipCard, TiltCard, GlowCard, ExpandCard } from './animated-card'; - -jest.mock('framer-motion', () => ({ - motion: { - div: ({ children, onHoverStart, onHoverEnd, onClick, ...props }: any) => ( -
- {children} -
- ), - }, -})); - -describe('Animated Cards', () => { - beforeEach(() => { - jest.clearAllMocks(); - }); - - describe('InkCard', () => { - it('should render ink card', () => { - render(Test Content); - expect(screen.getByText('Test Content')).toBeInTheDocument(); - }); - - it('should apply custom className', () => { - const { container } = render(Test); - expect(container.firstChild).toHaveClass('custom-class'); - }); - - it('should handle mouse move', () => { - const { container } = render(Test); - const card = container.firstChild as HTMLElement; - - fireEvent.mouseMove(card, { - clientX: 100, - clientY: 100, - }); - - expect(card).toBeInTheDocument(); - }); - - it('should handle hover events', () => { - const { container } = render(Test); - const card = container.firstChild as HTMLElement; - - fireEvent.mouseEnter(card); - fireEvent.mouseLeave(card); - - expect(card).toBeInTheDocument(); - }); - }); - - describe('GeometricCard', () => { - it('should render geometric card', () => { - render(Test Content); - expect(screen.getByText('Test Content')).toBeInTheDocument(); - }); - - it('should apply custom className', () => { - const { container } = render(Test); - expect(container.firstChild).toHaveClass('custom-class'); - }); - - it('should have corner decorations', () => { - const { container } = render(Test); - const corners = container.querySelectorAll('.absolute'); - expect(corners.length).toBeGreaterThan(0); - }); - }); - - describe('FlipCard', () => { - it('should render flip card', () => { - render( - - ); - expect(screen.getByText('Front')).toBeInTheDocument(); - expect(screen.getByText('Back')).toBeInTheDocument(); - }); - - it('should flip on click', () => { - const { container } = render( - - ); - - const card = container.firstChild as HTMLElement; - fireEvent.click(card); - - expect(card).toBeInTheDocument(); - }); - - it('should apply custom className', () => { - const { container } = render( - - ); - expect(container.firstChild).toHaveClass('custom-class'); - }); - }); - - describe('TiltCard', () => { - it('should render tilt card', () => { - render(Test Content); - expect(screen.getByText('Test Content')).toBeInTheDocument(); - }); - - it('should apply custom className', () => { - const { container } = render(Test); - expect(container.firstChild).toHaveClass('custom-class'); - }); - - it('should handle mouse move', () => { - const { container } = render(Test); - const card = container.firstChild as HTMLElement; - - fireEvent.mouseMove(card, { - clientX: 100, - clientY: 100, - }); - - expect(card).toBeInTheDocument(); - }); - - it('should handle mouse leave', () => { - const { container } = render(Test); - const card = container.firstChild as HTMLElement; - - fireEvent.mouseLeave(card); - - expect(card).toBeInTheDocument(); - }); - }); - - describe('GlowCard', () => { - it('should render glow card', () => { - render(Test Content); - expect(screen.getByText('Test Content')).toBeInTheDocument(); - }); - - it('should apply custom className', () => { - const { container } = render(Test); - expect(container.firstChild).toHaveClass('custom-class'); - }); - - it('should handle mouse move', () => { - const { container } = render(Test); - const card = container.firstChild as HTMLElement; - - fireEvent.mouseMove(card, { - clientX: 100, - clientY: 100, - }); - - expect(card).toBeInTheDocument(); - }); - }); - - describe('ExpandCard', () => { - it('should render expand card', () => { - render(Test Content); - expect(screen.getByText('Test Content')).toBeInTheDocument(); - }); - - it('should apply custom className', () => { - const { container } = render(Test); - expect(container.firstChild).toHaveClass('custom-class'); - }); - - it('should expand on click', () => { - const { container } = render( - Expanded}>Test - ); - - const card = container.firstChild as HTMLElement; - fireEvent.click(card); - - expect(screen.getByText('Expanded')).toBeInTheDocument(); - }); - }); -}); diff --git a/src/components/ui/animated-card.tsx b/src/components/ui/animated-card.tsx deleted file mode 100644 index 1db54be..0000000 --- a/src/components/ui/animated-card.tsx +++ /dev/null @@ -1,376 +0,0 @@ -'use client'; - -import { motion, type HTMLMotionProps, type Variants } from 'framer-motion'; -import { useRef, useState, type ReactNode, type MouseEvent } from 'react'; -import { cn } from '@/lib/utils'; - -const cardVariants: Variants = { - hidden: { - opacity: 0, - y: 30, - scale: 0.95, - }, - visible: { - opacity: 1, - y: 0, - scale: 1, - transition: { - duration: 0.6, - ease: [0.16, 1, 0.3, 1], - }, - }, -}; - -interface InkCardProps extends HTMLMotionProps<'div'> { - children: ReactNode; - className?: string; - hoverScale?: number; - hoverRotate?: number; - inkColor?: string; - showInkOnHover?: boolean; -} - -export function InkCard({ - children, - className = '', - hoverScale = 1.02, - hoverRotate = 0, - inkColor = 'rgba(196, 30, 58, 0.05)', - showInkOnHover = true, - ...props -}: InkCardProps) { - const cardRef = useRef(null); - const [inkPosition, setInkPosition] = useState({ x: 50, y: 50 }); - const [isHovered, setIsHovered] = useState(false); - - const handleMouseMove = (e: MouseEvent) => { - if (!cardRef.current) {return;} - const rect = cardRef.current.getBoundingClientRect(); - const x = ((e.clientX - rect.left) / rect.width) * 100; - const y = ((e.clientY - rect.top) / rect.height) * 100; - setInkPosition({ x, y }); - }; - - return ( - setIsHovered(true)} - onHoverEnd={() => setIsHovered(false)} - onMouseMove={handleMouseMove} - transition={{ - type: 'spring', - stiffness: 300, - damping: 20, - }} - className={cn( - 'relative overflow-hidden bg-white border border-[#E5E5E5] rounded-xl transition-shadow duration-300', - 'hover:shadow-[0_12px_24px_rgba(28,28,28,0.08)]', - className - )} - {...props} - > - {showInkOnHover && ( - -
- - )} -
{children}
- - ); -} - -interface GeometricCardProps extends HTMLMotionProps<'div'> { - children: ReactNode; - className?: string; - cornerColor?: string; -} - -export function GeometricCard({ - children, - className = '', - cornerColor = '#C41E3A', - ...props -}: GeometricCardProps) { - return ( - -
-
-
-
- {children} - - ); -} - -interface FlipCardProps { - front: ReactNode; - back: ReactNode; - className?: string; - frontClassName?: string; - backClassName?: string; -} - -export function FlipCard({ - front, - back, - className = '', - frontClassName = '', - backClassName = '', -}: FlipCardProps) { - const [isFlipped, setIsFlipped] = useState(false); - - return ( - setIsFlipped(!isFlipped)} - onKeyDown={(e) => { - if (e.key === 'Enter' || e.key === ' ') { - e.preventDefault(); - setIsFlipped(!isFlipped); - } - }} - role="button" - tabIndex={0} - aria-pressed={isFlipped} - aria-label={isFlipped ? '点击查看正面' : '点击查看背面'} - style={{ perspective: 1000 }} - > - -
- {front} -
-
- {back} -
-
-
- ); -} - -interface TiltCardProps extends HTMLMotionProps<'div'> { - children: ReactNode; - className?: string; - maxTilt?: number; - scale?: number; -} - -export function TiltCard({ - children, - className = '', - maxTilt = 10, - scale = 1.02, - ...props -}: TiltCardProps) { - const cardRef = useRef(null); - const [rotateX, setRotateX] = useState(0); - const [rotateY, setRotateY] = useState(0); - - const handleMouseMove = (e: MouseEvent) => { - if (!cardRef.current) {return;} - const rect = cardRef.current.getBoundingClientRect(); - const centerX = rect.left + rect.width / 2; - const centerY = rect.top + rect.height / 2; - const mouseX = e.clientX - centerX; - const mouseY = e.clientY - centerY; - - const rotateXValue = (mouseY / (rect.height / 2)) * -maxTilt; - const rotateYValue = (mouseX / (rect.width / 2)) * maxTilt; - - setRotateX(rotateXValue); - setRotateY(rotateYValue); - }; - - const handleMouseLeave = () => { - setRotateX(0); - setRotateY(0); - }; - - return ( - - {children} - - ); -} - -interface GlowCardProps extends HTMLMotionProps<'div'> { - children: ReactNode; - className?: string; - glowColor?: string; -} - -export function GlowCard({ - children, - className = '', - glowColor = 'rgba(196, 30, 58, 0.15)', - ...props -}: GlowCardProps) { - const cardRef = useRef(null); - const [glowPosition, setGlowPosition] = useState({ x: 50, y: 50 }); - - const handleMouseMove = (e: MouseEvent) => { - if (!cardRef.current) {return;} - const rect = cardRef.current.getBoundingClientRect(); - const x = ((e.clientX - rect.left) / rect.width) * 100; - const y = ((e.clientY - rect.top) / rect.height) * 100; - setGlowPosition({ x, y }); - }; - - return ( - - -
{children}
-
- ); -} - -interface ExpandCardProps extends HTMLMotionProps<'div'> { - children: ReactNode; - className?: string; - expandedContent?: ReactNode; -} - -export function ExpandCard({ - children, - className = '', - expandedContent, - ...props -}: ExpandCardProps) { - const [isExpanded, setIsExpanded] = useState(false); - - return ( - setIsExpanded(!isExpanded)} - onKeyDown={(e) => { - if (e.key === 'Enter' || e.key === ' ') { - e.preventDefault(); - setIsExpanded(!isExpanded); - } - }} - role="button" - tabIndex={0} - aria-expanded={isExpanded} - aria-label={isExpanded ? '点击收起详情' : '点击展开详情'} - transition={{ type: 'spring', stiffness: 300, damping: 20 }} - className={cn( - 'relative overflow-hidden bg-white border border-[#E5E5E5] rounded-xl cursor-pointer', - 'transition-shadow duration-300', - 'hover:shadow-[0_12px_24px_rgba(28,28,28,0.08)]', - className - )} - {...props} - > -
{children}
- {expandedContent && ( - -
- {expandedContent} -
-
- )} -
- ); -} diff --git a/src/components/ui/glass-card.test.tsx b/src/components/ui/glass-card.test.tsx deleted file mode 100644 index 5b5c3f5..0000000 --- a/src/components/ui/glass-card.test.tsx +++ /dev/null @@ -1,74 +0,0 @@ -import { describe, it, expect, beforeEach, jest } from '@jest/globals'; -import { render, screen } from '@testing-library/react'; -import '@testing-library/jest-dom'; -import { GlassCard } from './glass-card'; - -describe('GlassCard', () => { - beforeEach(() => { - jest.clearAllMocks(); - }); - - describe('Rendering', () => { - it('should render glass card', () => { - const { container } = render(Test Content); - expect(container.firstChild).toBeInTheDocument(); - }); - - it('should render children', () => { - render(Test Content); - expect(screen.getByText('Test Content')).toBeInTheDocument(); - }); - - it('should apply custom className', () => { - const { container } = render(Test); - expect(container.firstChild).toHaveClass('custom-class'); - }); - }); - - describe('Variants', () => { - it('should render default variant', () => { - const { container } = render(Test); - expect(container.firstChild).toBeInTheDocument(); - }); - - it('should render elevated variant', () => { - const { container } = render(Test); - expect(container.firstChild).toBeInTheDocument(); - }); - - it('should render outline variant', () => { - const { container } = render(Test); - expect(container.firstChild).toBeInTheDocument(); - }); - - it('should render glow variant', () => { - const { container } = render(Test); - expect(container.firstChild).toBeInTheDocument(); - }); - }); - - describe('Styling', () => { - it('should have rounded class', () => { - const { container } = render(Test); - expect(container.firstChild).toHaveClass('rounded-2xl'); - }); - - it('should have border class', () => { - const { container } = render(Test); - expect(container.firstChild).toHaveClass('border'); - }); - - it('should have backdrop-blur class', () => { - const { container } = render(Test); - expect(container.firstChild).toHaveClass('backdrop-blur-xl'); - }); - }); - - describe('Forward Ref', () => { - it('should forward ref', () => { - const ref = { current: null }; - render(Test); - expect(ref.current).toBeInstanceOf(HTMLDivElement); - }); - }); -}); diff --git a/src/components/ui/glass-card.tsx b/src/components/ui/glass-card.tsx deleted file mode 100644 index 72ff740..0000000 --- a/src/components/ui/glass-card.tsx +++ /dev/null @@ -1,41 +0,0 @@ -import * as React from "react" -import { cva, type VariantProps } from "class-variance-authority" -import { cn } from "@/lib/utils" - -const glassCardVariants = cva( - "rounded-2xl border backdrop-blur-xl transition-all duration-300", - { - variants: { - variant: { - default: - "border-gray-800/50 bg-gray-900/50 hover:border-purple-500/50 hover:bg-gray-800/60 hover:shadow-[0_0_40px_rgba(139,92,246,0.15)] hover:-translate-y-1", - elevated: - "border-gray-800 bg-gray-900/80 shadow-2xl hover:border-purple-500/50 hover:shadow-purple-500/20 hover:-translate-y-2", - outline: - "border-gray-800/50 bg-transparent hover:border-purple-500/30 hover:bg-gray-900/30", - glow: - "border-gray-800/50 bg-gray-900/40 relative overflow-hidden hover:border-purple-500/40", - }, - }, - defaultVariants: { - variant: "default", - }, - } -) - -export interface GlassCardProps - extends React.HTMLAttributes, - VariantProps {} - -const GlassCard = React.forwardRef( - ({ className, variant, ...props }, ref) => ( -
- ) -) -GlassCard.displayName = "GlassCard" - -export { GlassCard, glassCardVariants } diff --git a/src/components/ui/particle-background.tsx b/src/components/ui/particle-background.tsx deleted file mode 100644 index 6d9ca23..0000000 --- a/src/components/ui/particle-background.tsx +++ /dev/null @@ -1,110 +0,0 @@ -'use client'; - -import { useEffect, useRef } from 'react'; - -interface Particle { - x: number; - y: number; - vx: number; - vy: number; - size: number; - opacity: number; -} - -interface ParticleBackgroundProps { - particleCount?: number; - className?: string; -} - -export function ParticleBackground({ - particleCount = 50, - className = '' -}: ParticleBackgroundProps) { - const canvasRef = useRef(null); - - useEffect(() => { - const canvas = canvasRef.current; - if (!canvas) {return;} - - const ctx = canvas.getContext('2d'); - if (!ctx) {return;} - - let animationFrameId: number; - let particles: Particle[] = []; - - const resizeCanvas = () => { - canvas.width = window.innerWidth; - canvas.height = window.innerHeight; - }; - - const createParticles = () => { - particles = []; - for (let i = 0; i < particleCount; i++) { - particles.push({ - x: Math.random() * canvas.width, - y: Math.random() * canvas.height, - vx: (Math.random() - 0.5) * 0.5, - vy: (Math.random() - 0.5) * 0.5, - size: Math.random() * 2 + 1, - opacity: Math.random() * 0.5 + 0.2, - }); - } - }; - - const drawParticles = () => { - ctx.clearRect(0, 0, canvas.width, canvas.height); - - particles.forEach((particle, i) => { - particle.x += particle.vx; - particle.y += particle.vy; - - if (particle.x < 0 || particle.x > canvas.width) {particle.vx *= -1;} - if (particle.y < 0 || particle.y > canvas.height) {particle.vy *= -1;} - - ctx.beginPath(); - ctx.arc(particle.x, particle.y, particle.size, 0, Math.PI * 2); - ctx.fillStyle = `rgba(0, 217, 255, ${particle.opacity})`; - ctx.fill(); - - particles.forEach((otherParticle, j) => { - if (i === j) {return;} - const dx = particle.x - otherParticle.x; - const dy = particle.y - otherParticle.y; - const distance = Math.sqrt(dx * dx + dy * dy); - - if (distance < 150) { - ctx.beginPath(); - ctx.moveTo(particle.x, particle.y); - ctx.lineTo(otherParticle.x, otherParticle.y); - ctx.strokeStyle = `rgba(0, 217, 255, ${0.1 * (1 - distance / 150)})`; - ctx.stroke(); - } - }); - }); - - animationFrameId = requestAnimationFrame(drawParticles); - }; - - resizeCanvas(); - createParticles(); - drawParticles(); - - window.addEventListener('resize', () => { - resizeCanvas(); - createParticles(); - }); - - return () => { - cancelAnimationFrame(animationFrameId); - window.removeEventListener('resize', resizeCanvas); - }; - }, [particleCount]); - - return ( - - ); -} diff --git a/src/components/ui/ripple-button.test.tsx b/src/components/ui/ripple-button.test.tsx deleted file mode 100644 index 3cee351..0000000 --- a/src/components/ui/ripple-button.test.tsx +++ /dev/null @@ -1,92 +0,0 @@ -import { render, screen, fireEvent } from '@testing-library/react'; -import '@testing-library/jest-dom'; - -const MockRippleButton = ({ - children, - onClick, - disabled, - variant, - size -}: { - children: React.ReactNode; - onClick?: () => void; - disabled?: boolean; - variant?: string; - size?: string; -}) => { - return ( - - ); -}; - -jest.mock('framer-motion', () => ({ - motion: { - div: ({ children, onClick }: { children: React.ReactNode; onClick?: () => void }) => ( -
{children}
- ), - }, - AnimatePresence: ({ children }: { children: React.ReactNode }) => <>{children}, -})); - -describe('RippleButton', () => { - beforeEach(() => { - jest.clearAllMocks(); - }); - - describe('Rendering', () => { - it('should render button', () => { - render(Click me); - const button = screen.getByRole('button', { name: /click me/i }); - expect(button).toBeInTheDocument(); - }); - - it('should render children', () => { - render(Test Button); - expect(screen.getByText('Test Button')).toBeInTheDocument(); - }); - - it('should apply variant classes', () => { - render(Secondary); - const button = screen.getByRole('button'); - expect(button).toHaveClass('secondary'); - }); - - it('should apply size classes', () => { - render(Large); - const button = screen.getByRole('button'); - expect(button).toHaveClass('lg'); - }); - }); - - describe('Functionality', () => { - it('should handle click events', () => { - const handleClick = jest.fn(); - render(Click me); - - const button = screen.getByRole('button'); - fireEvent.click(button); - - expect(handleClick).toHaveBeenCalled(); - }); - - it('should be disabled when disabled prop is true', () => { - render(Disabled); - const button = screen.getByRole('button'); - expect(button).toBeDisabled(); - }); - }); - - describe('Accessibility', () => { - it('should be focusable', () => { - render(Focus me); - const button = screen.getByRole('button'); - expect(button).not.toHaveAttribute('tabindex', '-1'); - }); - }); -}); diff --git a/src/components/ui/ripple-button.tsx b/src/components/ui/ripple-button.tsx deleted file mode 100644 index bbdcdbe..0000000 --- a/src/components/ui/ripple-button.tsx +++ /dev/null @@ -1,193 +0,0 @@ -'use client'; - -import * as React from 'react'; -import { motion, AnimatePresence } from 'framer-motion'; -import { cva, type VariantProps } from 'class-variance-authority'; -import { cn } from '@/lib/utils'; - -const rippleButtonVariants = cva( - 'inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-lg text-sm font-medium transition-all duration-300 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*=\'size-\'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:ring-2 focus-visible:ring-[#1C1C1C] focus-visible:ring-offset-2 focus-visible:ring-offset-white relative overflow-hidden', - { - variants: { - variant: { - default: - 'bg-[#C41E3A] text-white hover:bg-[#A01830] hover:shadow-[0_8px_20px_rgba(196,30,58,0.35)]', - secondary: - 'bg-[#1C1C1C] text-white hover:bg-[#0A0A0A] hover:shadow-[0_8px_20px_rgba(28,28,28,0.35)]', - destructive: - 'bg-[#C41E3A] text-white hover:bg-[#A01830] focus-visible:ring-[#C41E3A]', - outline: - 'border-2 border-[#1C1C1C] bg-transparent text-[#1C1C1C] hover:bg-[#F5F5F5] hover:shadow-[0_4px_12px_rgba(28,28,28,0.2)]', - ghost: - 'text-[#3D3D3D] hover:bg-[#F5F5F5] hover:text-[#1C1C1C]', - link: - 'text-[#1C1C1C] underline-offset-4 hover:underline hover:text-[#C41E3A]', - seal: - 'bg-[#C41E3A] text-white font-semibold hover:bg-[#A01830] shadow-[0_6px_20px_rgba(196,30,58,0.3)]', - }, - size: { - default: 'h-11 px-4 py-2.5', - sm: 'h-9 rounded-md px-3 text-xs', - lg: 'h-12 rounded-lg px-6 text-base', - icon: 'h-11 w-11', - }, - }, - defaultVariants: { - variant: 'default', - size: 'default', - }, - } -); - -interface Ripple { - x: number; - y: number; - id: number; -} - -export interface RippleButtonProps - extends VariantProps { - rippleColor?: string; - rippleDuration?: number; - children?: React.ReactNode; - onClick?: (e: React.MouseEvent) => void; - onKeyDown?: (e: React.KeyboardEvent) => void; - className?: string; - disabled?: boolean; -} - -const RippleButton = React.forwardRef( - ({ className, variant, size, rippleColor, rippleDuration = 800, onClick, children, disabled, ...props }, ref) => { - const [ripples, setRipples] = React.useState([]); - - const handleClick = (e: React.MouseEvent) => { - if (disabled) {return;} - - const button = e.currentTarget; - const rect = button.getBoundingClientRect(); - const x = e.clientX - rect.left; - const y = e.clientY - rect.top; - const id = Date.now(); - - setRipples((prev) => [...prev, { x, y, id }]); - - setTimeout(() => { - setRipples((prev) => prev.filter((r) => r.id !== id)); - }, rippleDuration); - - onClick?.(e); - }; - - const getRippleColor = () => { - if (rippleColor) {return rippleColor;} - if (variant === 'outline' || variant === 'ghost' || variant === 'link') { - return 'rgba(196, 30, 58, 0.2)'; - } - return 'rgba(255, 255, 255, 0.4)'; - }; - - return ( - - {children} - - {ripples.map((ripple) => ( - - ))} - - - ); - } -); -RippleButton.displayName = 'RippleButton'; - -export interface SealButtonProps extends VariantProps { - children?: React.ReactNode; - onClick?: (e: React.MouseEvent) => void; - onKeyDown?: (e: React.KeyboardEvent) => void; - className?: string; - disabled?: boolean; -} - -const SealButton = React.forwardRef( - ({ className, variant = 'seal', size, onClick, children, disabled, ...props }, ref) => { - const [isPressed, setIsPressed] = React.useState(false); - const [showInk, setShowInk] = React.useState(false); - - const handleClick = (e: React.MouseEvent) => { - if (disabled) {return;} - setIsPressed(true); - setShowInk(true); - setTimeout(() => setIsPressed(false), 600); - setTimeout(() => setShowInk(false), 800); - onClick?.(e); - }; - - return ( - - - {showInk && ( - -
- - )} - - {children} - - ); - } -); -SealButton.displayName = 'SealButton'; - -export { RippleButton, SealButton, rippleButtonVariants }; diff --git a/src/components/ui/testimonial-card.test.tsx b/src/components/ui/testimonial-card.test.tsx deleted file mode 100644 index 0f068be..0000000 --- a/src/components/ui/testimonial-card.test.tsx +++ /dev/null @@ -1,108 +0,0 @@ -import { describe, it, expect, beforeEach, jest } from '@jest/globals'; -import { render, screen } from '@testing-library/react'; -import '@testing-library/jest-dom'; -import { TestimonialCard } from './testimonial-card'; - -jest.mock('next/image', () => ({ - __esModule: true, - default: ({ src, alt, width, height, className }: any) => ( - {alt} - ), -})); - -describe('TestimonialCard', () => { - const defaultProps = { - quote: 'Test quote', - author: 'Test Author', - position: 'Manager', - company: 'Test Company', - }; - - beforeEach(() => { - jest.clearAllMocks(); - }); - - describe('Rendering', () => { - it('should render testimonial card', () => { - render(); - const blockquote = screen.getByText((content, element) => { - return element?.tagName === 'BLOCKQUOTE' && content.includes('Test quote'); - }); - expect(blockquote).toBeInTheDocument(); - }); - - it('should render author name', () => { - render(); - expect(screen.getByText('Test Author')).toBeInTheDocument(); - }); - - it('should render position and company', () => { - render(); - expect(screen.getByText(/Manager · Test Company/)).toBeInTheDocument(); - }); - - it('should render quote with quotes', () => { - render(); - const blockquote = screen.getByText((content, element) => { - return element?.tagName === 'BLOCKQUOTE' && content.includes('Test quote'); - }); - expect(blockquote).toBeInTheDocument(); - }); - }); - - describe('Rating', () => { - it('should render 5 stars by default', () => { - render(); - const stars = document.querySelectorAll('svg.w-4.h-4'); - expect(stars).toHaveLength(5); - }); - - it('should render custom rating', () => { - render(); - const stars = document.querySelectorAll('svg.w-4.h-4'); - expect(stars).toHaveLength(3); - }); - - it('should not render stars when rating is 0', () => { - render(); - const stars = document.querySelectorAll('svg.w-4.h-4'); - expect(stars).toHaveLength(0); - }); - }); - - describe('Avatar', () => { - it('should render avatar when avatarUrl is provided', () => { - render(); - const avatar = screen.getByAltText('Test Author'); - expect(avatar).toBeInTheDocument(); - }); - - it('should not render avatar when avatarUrl is not provided', () => { - render(); - const avatar = screen.queryByAltText('Test Author'); - expect(avatar).not.toBeInTheDocument(); - }); - }); - - describe('Styling', () => { - it('should have correct card classes', () => { - const { container } = render(); - const card = container.firstChild as HTMLElement; - expect(card).toHaveClass('relative'); - expect(card).toHaveClass('p-8'); - expect(card).toHaveClass('rounded-lg'); - }); - - it('should have border class', () => { - const { container } = render(); - const card = container.firstChild as HTMLElement; - expect(card).toHaveClass('border'); - }); - - it('should have background class', () => { - const { container } = render(); - const card = container.firstChild as HTMLElement; - expect(card.className).toContain('bg-white'); - }); - }); -}); diff --git a/src/components/ui/testimonial-card.tsx b/src/components/ui/testimonial-card.tsx deleted file mode 100644 index 17cb00f..0000000 --- a/src/components/ui/testimonial-card.tsx +++ /dev/null @@ -1,65 +0,0 @@ -'use client'; - -import Image from 'next/image'; -import { Quote } from 'lucide-react'; - -export interface TestimonialCardProps { - quote: string; - author: string; - position: string; - company: string; - avatarUrl?: string; - rating?: number; -} - -export function TestimonialCard({ - quote, - author, - position, - company, - avatarUrl, - rating = 5, -}: TestimonialCardProps) { - return ( -
- - - {rating > 0 && ( -
- {Array.from({ length: rating }).map((_, i) => ( - - - - ))} -
- )} - -
- “{quote}” -
- -
- {avatarUrl && ( - {author} - )} -
-
{author}
-
- {position} · {company} -
-
-
-
- ); -} diff --git a/src/components/ui/touch-optimized.tsx b/src/components/ui/touch-optimized.tsx deleted file mode 100644 index fbce0e2..0000000 --- a/src/components/ui/touch-optimized.tsx +++ /dev/null @@ -1,269 +0,0 @@ -'use client'; - -import { useState, useCallback, useRef, useEffect, memo } from 'react'; -import { cn } from '@/lib/utils'; - -interface SwipeableProps { - children: React.ReactNode; - onSwipeLeft?: () => void; - onSwipeRight?: () => void; - onSwipeUp?: () => void; - onSwipeDown?: () => void; - threshold?: number; - className?: string; - disabled?: boolean; -} - -export const Swipeable = memo(function Swipeable({ - children, - onSwipeLeft, - onSwipeRight, - onSwipeUp, - onSwipeDown, - threshold = 50, - className, - disabled = false, -}: SwipeableProps) { - const [touchStart, setTouchStart] = useState<{ x: number; y: number } | null>(null); - const [touchEnd, setTouchEnd] = useState<{ x: number; y: number } | null>(null); - const ref = useRef(null); - - const onTouchStart = useCallback((e: React.TouchEvent) => { - if (disabled) return; - const touch = e.targetTouches[0]; - if (!touch) return; - setTouchEnd(null); - setTouchStart({ - x: touch.clientX, - y: touch.clientY, - }); - }, [disabled]); - - const onTouchMove = useCallback((e: React.TouchEvent) => { - if (disabled) return; - const touch = e.targetTouches[0]; - if (!touch) return; - setTouchEnd({ - x: touch.clientX, - y: touch.clientY, - }); - }, [disabled]); - - const onTouchEnd = useCallback(() => { - if (!touchStart || !touchEnd || disabled) return; - - const distanceX = touchStart.x - touchEnd.x; - const distanceY = touchStart.y - touchEnd.y; - const isHorizontalSwipe = Math.abs(distanceX) > Math.abs(distanceY); - - if (isHorizontalSwipe) { - if (Math.abs(distanceX) > threshold) { - if (distanceX > 0) { - onSwipeLeft?.(); - } else { - onSwipeRight?.(); - } - } - } else { - if (Math.abs(distanceY) > threshold) { - if (distanceY > 0) { - onSwipeUp?.(); - } else { - onSwipeDown?.(); - } - } - } - }, [touchStart, touchEnd, threshold, onSwipeLeft, onSwipeRight, onSwipeUp, onSwipeDown, disabled]); - - return ( -
- {children} -
- ); -}); - -interface PullToRefreshProps { - children: React.ReactNode; - onRefresh: () => Promise; - disabled?: boolean; - className?: string; -} - -export const PullToRefresh = memo(function PullToRefresh({ - children, - onRefresh, - disabled = false, - className, -}: PullToRefreshProps) { - const [isRefreshing, setIsRefreshing] = useState(false); - const [pullDistance, setPullDistance] = useState(0); - const touchStartY = useRef(0); - const containerRef = useRef(null); - - const handleTouchStart = useCallback((e: React.TouchEvent) => { - if (disabled || isRefreshing) return; - const touch = e.touches[0]; - if (touch) { - touchStartY.current = touch.clientY; - } - }, [disabled, isRefreshing]); - - const handleTouchMove = useCallback((e: React.TouchEvent) => { - if (disabled || isRefreshing) return; - - const container = containerRef.current; - if (!container || container.scrollTop > 0) return; - - const touch = e.touches[0]; - if (!touch) return; - const distance = touch.clientY - touchStartY.current; - - if (distance > 0) { - setPullDistance(Math.min(distance, 100)); - } - }, [disabled, isRefreshing]); - - const handleTouchEnd = useCallback(async () => { - if (disabled || isRefreshing) return; - - if (pullDistance > 60) { - setIsRefreshing(true); - try { - await onRefresh(); - } finally { - setIsRefreshing(false); - } - } - setPullDistance(0); - }, [disabled, isRefreshing, pullDistance, onRefresh]); - - return ( -
- {pullDistance > 0 && ( -
-
-
- )} - {children} -
- ); -}); - -interface TouchFeedbackProps { - children: React.ReactNode; - className?: string; - disabled?: boolean; -} - -export const TouchFeedback = memo(function TouchFeedback({ - children, - className, - disabled = false, -}: TouchFeedbackProps) { - const [isPressed, setIsPressed] = useState(false); - - return ( -
!disabled && setIsPressed(true)} - onTouchEnd={() => setIsPressed(false)} - onTouchCancel={() => setIsPressed(false)} - > - {children} -
- ); -}); - -interface LongPressProps { - children: React.ReactNode; - onLongPress: () => void; - delay?: number; - className?: string; - disabled?: boolean; -} - -export const LongPress = memo(function LongPress({ - children, - onLongPress, - delay = 500, - className, - disabled = false, -}: LongPressProps) { - const timeoutRef = useRef(null); - const [isPressed, setIsPressed] = useState(false); - - const handleTouchStart = useCallback(() => { - if (disabled) return; - setIsPressed(true); - timeoutRef.current = setTimeout(() => { - onLongPress(); - setIsPressed(false); - }, delay); - }, [disabled, delay, onLongPress]); - - const handleTouchEnd = useCallback(() => { - if (timeoutRef.current) { - clearTimeout(timeoutRef.current); - timeoutRef.current = null; - } - setIsPressed(false); - }, []); - - useEffect(() => { - return () => { - if (timeoutRef.current) { - clearTimeout(timeoutRef.current); - } - }; - }, []); - - return ( -
- {children} -
- ); -}); - -export function useTouchDevice() { - const [isTouchDevice, setIsTouchDevice] = useState(false); - - useEffect(() => { - setIsTouchDevice( - 'ontouchstart' in window || - navigator.maxTouchPoints > 0 - ); - }, []); - - return isTouchDevice; -}