first commit

This commit is contained in:
“dongming”
2025-12-19 17:06:23 +08:00
commit a146846cc0
63 changed files with 2952 additions and 0 deletions

View File

@@ -0,0 +1,42 @@
import { missionVisionData, commonDescriptions } from '../../data/siteData'
export default function Mission() {
return (
<section className="py-20 bg-white" id="features">
<div className="container mx-auto px-4">
<div className="flex flex-wrap lg:flex-nowrap gap-12">
{/* Left - Feature Cards */}
<div className="w-full lg:w-2/3 grid sm:grid-cols-2 gap-6 lg:pr-8">
{missionVisionData.map((item) => (
<div key={item.id} className="feature-gd">
<img
src={item.image}
alt={item.title}
className="w-full h-auto object-cover rounded"
/>
<div className="icon-info mt-3">
<h5 className="text-lg font-semibold mt-3 text-title">
{item.title}
</h5>
<p className="text-text mt-2">
{item.description}
</p>
</div>
</div>
))}
</div>
{/* Right Content */}
<div className="w-full lg:w-1/3">
<h6 className="text-secondary font-semibold mb-3">
{commonDescriptions.aboutQuote}
</h6>
<p className="text-text mt-3 leading-relaxed">
{commonDescriptions.aboutDesc}
</p>
</div>
</div>
</div>
</section>
)
}

View File

@@ -0,0 +1,70 @@
import { useState, useEffect, useRef } from 'react'
import { aboutStatsData } from '../../data/siteData'
export default function Statistics() {
const [isVisible, setIsVisible] = useState(false)
const sectionRef = useRef<HTMLElement>(null)
useEffect(() => {
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) {
setIsVisible(true)
}
},
{ threshold: 0.3 }
)
if (sectionRef.current) {
observer.observe(sectionRef.current)
}
return () => observer.disconnect()
}, [])
return (
<section
ref={sectionRef}
className="py-16"
id="stats"
style={{ background: 'linear-gradient(100deg, #2e5deb 10%, #5360fd 50%, #ff5b83 100%)' }}
>
<div className="container mx-auto px-4 py-3">
<div className="grid grid-cols-2 lg:grid-cols-4 gap-8">
{aboutStatsData.map((stat) => (
<div key={stat.id} className="text-center">
<span className={`fa fa-${stat.icon} text-secondary mb-3 block`} style={{ fontSize: '40px' }} />
<h3 className="text-[50px] font-bold font-sans mb-1" style={{ color: '#ffffff' }}>
<CountUp end={stat.value} isVisible={isVisible} />
</h3>
<p className="text-white">{stat.label}</p>
</div>
))}
</div>
</div>
</section>
)
}
function CountUp({ end, isVisible }: { end: number; isVisible: boolean }) {
const [count, setCount] = useState(0)
useEffect(() => {
if (!isVisible) return
let startTime: number
const duration = 2000
const step = (timestamp: number) => {
if (!startTime) startTime = timestamp
const progress = Math.min((timestamp - startTime) / duration, 1)
setCount(Math.floor(progress * end))
if (progress < 1) {
requestAnimationFrame(step)
}
}
requestAnimationFrame(step)
}, [end, isVisible])
return <>{count.toLocaleString()}</>
}

View File

@@ -0,0 +1,63 @@
import { teamData, commonDescriptions } from '../../data/siteData'
export default function Team() {
return (
<section className="py-20 bg-white" id="team">
<div className="container mx-auto px-4">
{/* Section Header */}
<div className="text-center max-w-3xl mx-auto mb-12">
<h3 className="text-3xl md:text-4xl font-bold text-title mb-4 font-sans">
{commonDescriptions.teamTitle}
</h3>
<p className="text-text my-3">
{commonDescriptions.sectionDesc}
</p>
</div>
{/* Team Grid */}
<div className="grid grid-cols-2 lg:grid-cols-4 gap-6 pt-5 mt-5">
{teamData.map((member) => (
<div
key={member.id}
className="team-info text-center"
>
<div className="column relative">
<a href="#url">
<img
src={member.image}
alt={member.name}
className="w-full h-auto object-cover rounded"
/>
</a>
</div>
<div className="column mt-4">
<h3 className="text-lg font-semibold font-sans">
<a href="#url" className="text-title hover:text-secondary transition-colors">
{member.name}
</a>
</h3>
<p className="text-text">{member.role}</p>
<div className="social mt-3">
<div className="social-left flex justify-center gap-2">
<a href={member.social.facebook} className="w-10 h-10 bg-secondary hover:bg-primary rounded flex items-center justify-center text-white transition-colors">
<span className="fa fa-facebook" aria-hidden="true" />
</a>
<a href={member.social.twitter} className="w-10 h-10 bg-secondary hover:bg-primary rounded flex items-center justify-center text-white transition-colors">
<span className="fa fa-twitter" aria-hidden="true" />
</a>
<a href={member.social.linkedin} className="w-10 h-10 bg-secondary hover:bg-primary rounded flex items-center justify-center text-white transition-colors">
<span className="fa fa-linkedin" aria-hidden="true" />
</a>
<a href={member.social.google} className="w-10 h-10 bg-secondary hover:bg-primary rounded flex items-center justify-center text-white transition-colors">
<span className="fa fa-google-plus" aria-hidden="true" />
</a>
</div>
</div>
</div>
</div>
))}
</div>
</div>
</section>
)
}

View File

@@ -0,0 +1,39 @@
import { whyChooseUsData, commonDescriptions } from '../../data/siteData'
export default function WhyChooseUs() {
return (
<section className="py-20 bg-white" id="about">
<div className="container mx-auto px-4">
<div className="flex flex-wrap lg:flex-nowrap gap-12 items-center">
{/* Left - Image */}
<div className="w-full lg:w-1/2">
<img
src="/src/assets/images/g5.jpg"
alt="Why Choose Us"
className="w-full rounded-lg"
/>
</div>
{/* Right Content */}
<div className="w-full lg:w-1/2 lg:pl-8">
<h3 className="text-3xl md:text-4xl font-bold text-title mb-4 font-sans">
{commonDescriptions.whyChooseUsTitle}
</h3>
<p className="text-text mb-8 leading-relaxed">
{commonDescriptions.whyChooseUsDesc}
</p>
<ul className="cont-4 space-y-3">
{whyChooseUsData.map((item, index) => (
<li key={index} className="flex items-center gap-3">
<span className="fa fa-check text-secondary" />
<span className="text-text">{item}</span>
</li>
))}
</ul>
</div>
</div>
</div>
</section>
)
}

View File

@@ -0,0 +1,120 @@
import { useState } from 'react'
import { contactFormInfo } from '../../data/siteData'
export default function ContactForm() {
const [formData, setFormData] = useState({
name: '',
email: '',
subject: '',
message: '',
})
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault()
console.log('Form submitted:', formData)
alert('Message sent successfully!')
setFormData({ name: '', email: '', subject: '', message: '' })
}
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
setFormData({
...formData,
[e.target.name]: e.target.value,
})
}
return (
<section className="py-20 bg-white" id="contact">
<div className="container mx-auto px-4">
<div className="grid lg:grid-cols-2 gap-12">
{/* Contact Info */}
<div className="contact-left">
<h4 className="text-2xl font-bold text-title mb-4 font-sans">
{contactFormInfo.title}
</h4>
<h6 className="text-text mb-6">
{contactFormInfo.subtitle}
</h6>
<div className="hours space-y-4">
<div>
<h6 className="font-semibold text-title mt-3">Email:</h6>
<p>
<a href={`mailto:${contactFormInfo.email}`} className="text-text hover:text-primary transition-colors">
{contactFormInfo.email}
</a>
</p>
</div>
<div>
<h6 className="font-semibold text-title mt-3">Address:</h6>
<p className="text-text">{contactFormInfo.address}</p>
</div>
<div>
<h6 className="font-semibold text-title mt-3">Contact:</h6>
<p>
<a href={`tel:${contactFormInfo.phone}`} className="text-text hover:text-primary transition-colors">
{contactFormInfo.phone}
</a>
</p>
</div>
</div>
</div>
{/* Contact Form */}
<div className="contact-right">
<form onSubmit={handleSubmit} className="signin-form">
<div className="input-grids space-y-4">
<input
type="text"
name="name"
id="w3lName"
value={formData.name}
onChange={handleChange}
placeholder="Your Name*"
required
className="contact-input"
/>
<input
type="email"
name="email"
id="w3lSender"
value={formData.email}
onChange={handleChange}
placeholder="Your Email*"
required
className="contact-input"
/>
<input
type="text"
name="subject"
id="w3lSubect"
value={formData.subject}
onChange={handleChange}
placeholder="Subject*"
className="contact-input"
/>
</div>
<div className="form-input mt-4">
<textarea
name="message"
id="w3lMessage"
value={formData.message}
onChange={handleChange}
placeholder="Type your message here*"
rows={5}
required
className="contact-input resize-none"
/>
</div>
<button
type="submit"
className="mt-6 px-8 py-3 bg-secondary hover:bg-secondary/80 text-white font-semibold rounded transition-colors"
>
Send Message
</button>
</form>
</div>
</div>
</div>
</section>
)
}

View File

@@ -0,0 +1,47 @@
export default function Map() {
return (
<section className="h-96 bg-gray-200 relative overflow-hidden">
{/* 静态地图占位图 */}
<div className="absolute inset-0 flex items-center justify-center bg-gradient-to-br from-gray-100 to-gray-300">
{/* 地图图标 */}
<div className="text-center">
<svg
className="w-24 h-24 mx-auto text-gray-400 mb-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={1}
d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z"
/>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={1}
d="M15 11a3 3 0 11-6 0 3 3 0 016 0z"
/>
</svg>
<p className="text-gray-500 text-lg font-medium">Location Map</p>
<p className="text-gray-400 text-sm mt-2">
Lorem ipsum, #32841 block, #221DRS Estate business building, UK
</p>
</div>
</div>
{/* 装饰性网格线 */}
<div
className="absolute inset-0 opacity-10"
style={{
backgroundImage: `
linear-gradient(rgba(0,0,0,0.1) 1px, transparent 1px),
linear-gradient(90deg, rgba(0,0,0,0.1) 1px, transparent 1px)
`,
backgroundSize: '50px 50px'
}}
/>
</section>
)
}

View File

@@ -0,0 +1,36 @@
import { Link } from '@tanstack/react-router'
import { commonDescriptions } from '../../data/siteData'
export default function CTA() {
return (
<section
className="py-16"
style={{ background: 'linear-gradient(100deg, #2e5deb 10%, #5360fd 50%, #ff5b83 100%)' }}
>
<div className="container mx-auto px-4 py-3 text-center">
<div className="max-w-3xl mx-auto">
<h3 className="text-2xl md:text-3xl font-bold mb-4 font-sans" style={{ color: '#ffffff' }}>
{commonDescriptions.ctaTitle}
</h3>
<p className="text-white my-3">
{commonDescriptions.sectionDesc}
</p>
</div>
<div className="flex flex-wrap justify-center gap-4 mt-8">
<Link
to="/contact"
className="px-8 py-3 border-2 border-white text-white hover:bg-secondary hover:border-secondary font-semibold rounded transition-colors"
>
Contact Us
</Link>
<Link
to="/contact"
className="px-8 py-3 bg-primary hover:bg-secondary text-white font-semibold rounded transition-colors"
>
Get Started
</Link>
</div>
</div>
</section>
)
}

View File

@@ -0,0 +1,51 @@
import { Link } from '@tanstack/react-router'
import { featuresData, commonDescriptions } from '../../data/siteData'
export default function Features() {
return (
<section className="py-20 bg-white" id="about">
<div className="container mx-auto px-4">
<div className="flex flex-wrap lg:flex-nowrap gap-12">
{/* Left Content */}
<div className="w-full lg:w-1/3">
<h4 className="text-lg font-semibold text-secondary mb-3">
{commonDescriptions.featuresSubtitle}
</h4>
<p className="text-text mt-3 leading-relaxed">
{commonDescriptions.featuresDesc}
</p>
<Link
to="/services"
className="inline-block mt-4 px-6 py-3 bg-secondary hover:bg-secondary/80 text-white font-semibold rounded transition-colors"
>
See More Services &rarr;
</Link>
</div>
{/* Right Content - Feature Cards */}
<div className="w-full lg:w-2/3 grid sm:grid-cols-2 gap-6 lg:pl-8">
{featuresData.map((feature) => (
<div key={feature.id} className="feature-gd">
<img
src={feature.image}
alt={feature.title}
className="w-full h-auto object-cover rounded"
/>
<div className="icon-info mt-3">
<h5 className="text-lg font-semibold mt-3">
<a href="#" className="text-title hover:text-secondary transition-colors">
{feature.title}
</a>
</h5>
<p className="text-text mt-2">
{feature.description}
</p>
</div>
</div>
))}
</div>
</div>
</div>
</section>
)
}

View File

@@ -0,0 +1,70 @@
import { Swiper, SwiperSlide } from 'swiper/react'
import { Navigation, Pagination, Autoplay, EffectFade } from 'swiper/modules'
import { Link } from '@tanstack/react-router'
import { sliderData } from '../../data/siteData'
import 'swiper/css'
import 'swiper/css/navigation'
import 'swiper/css/pagination'
import 'swiper/css/effect-fade'
// 背景图片映射
const bgImages: Record<string, string> = {
'bg-slider-1': '/src/assets/images/1.jpg',
'bg-slider-2': '/src/assets/images/2.jpg',
'bg-slider-3': '/src/assets/images/4.jpg',
'bg-slider-4': '/src/assets/images/5.jpg',
}
export default function HeroSlider() {
return (
<section className="relative" id="home">
<Swiper
modules={[Navigation, Pagination, Autoplay, EffectFade]}
navigation
pagination={{ clickable: true }}
autoplay={{ delay: 5000, disableOnInteraction: false }}
effect="fade"
loop
className="hero-swiper"
>
{sliderData.map((slide) => (
<SwiperSlide key={slide.id}>
<div
className="relative h-screen min-h-[600px] flex items-center justify-center bg-cover bg-center"
style={{
backgroundImage: `linear-gradient(rgba(0, 0, 0, 0.5), rgba(0, 0, 0, 0.5)), url(${bgImages[slide.bgClass] || '/src/assets/images/1.jpg'})`,
}}
>
<div className="container mx-auto px-4 text-center">
<div className="max-w-3xl mx-auto">
<h1
className="text-3xl md:text-4xl lg:text-5xl font-bold mb-8 font-sans leading-tight"
style={{ color: '#ffffff' }}
>
{slide.title}
</h1>
<Link
to={slide.buttonLink}
className="inline-block px-8 py-4 bg-secondary hover:bg-secondary/80 text-white font-semibold rounded transition-colors"
>
{slide.buttonText}
</Link>
{/* Scroll Indicator - 原始 HTML 滚动鼠标动画 */}
<div className="mt-16">
<a href="#about" className="inline-block">
<div className="icon-scroll">
<div className="wheel" />
</div>
</a>
</div>
</div>
</div>
</div>
</SwiperSlide>
))}
</Swiper>
</section>
)
}

View File

@@ -0,0 +1,65 @@
import { blogData, commonDescriptions } from '../../data/siteData'
export default function LatestNews() {
return (
<section className="py-20 bg-white" id="news">
<div className="container mx-auto px-4">
{/* Section Header */}
<div className="text-center max-w-3xl mx-auto mb-8">
<h3 className="text-3xl md:text-4xl font-bold text-title mb-4 font-sans">
{commonDescriptions.newsTitle}
</h3>
<p className="text-text my-3">
{commonDescriptions.sectionDesc}
</p>
</div>
{/* Blog Grid */}
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-6">
{blogData.map((post, index) => (
<div
key={post.id}
className={`mt-4 ${
index === 2 ? 'md:col-start-2 lg:col-start-auto' : ''
}`}
>
<div className="grids5-info bg-white rounded-lg overflow-hidden shadow-card hover:shadow-card-hover transition-shadow">
<a href={post.link} className="d-block zoom block overflow-hidden">
<img
src={post.image}
alt={post.title}
className="w-full h-48 object-cover transition-transform duration-500 hover:scale-110"
/>
</a>
<div className="blog-info p-6">
<ul className="flex gap-2 mb-2">
{post.categories.map((cat, i) => (
<li key={i}>
<a href="#" className="text-secondary hover:text-primary text-sm">
{cat}{i < post.categories.length - 1 ? ',' : ''}
</a>
</li>
))}
</ul>
<p className="text-text/60 text-sm mb-2">{post.date}</p>
<h4 className="text-lg font-bold mb-3">
<a href={post.link} className="text-title hover:text-secondary transition-colors">
{post.title}
</a>
</h4>
<p className="text-text mb-4">{post.description}</p>
<a
href={post.link}
className="text-secondary hover:text-primary font-medium transition-colors"
>
Read More <span className="fa fa-angle-right pl-1" />
</a>
</div>
</div>
</div>
))}
</div>
</div>
</section>
)
}

View File

@@ -0,0 +1,51 @@
import { servicesData, commonDescriptions } from '../../data/siteData'
export default function Services() {
return (
<section className="py-20 bg-services-bg" id="services">
<div className="container mx-auto px-4">
{/* Section Header */}
<div className="text-center max-w-3xl mx-auto mb-12">
<h3 className="text-3xl md:text-4xl font-bold text-title mb-4 font-sans">
{commonDescriptions.servicesTitle}
</h3>
<p className="text-text my-3">
{commonDescriptions.sectionDesc}
</p>
</div>
{/* Service Cards */}
<div className="grid md:grid-cols-2 lg:grid-cols-4 gap-6 pt-8 mt-3">
{servicesData.map((service) => (
<div
key={service.id}
className="group bg-white text-center px-5 py-10 rounded border-b-[3px] border-secondary hover:bg-secondary transition-all duration-300"
>
<div className="icon-holder mb-4">
<span
className={`fa fa-${service.icon} text-secondary group-hover:text-white transition-colors duration-300`}
style={{ fontSize: '36px' }}
aria-hidden="true"
/>
</div>
<h4 className="text-xl font-semibold text-title group-hover:text-white mb-3 font-sans transition-colors duration-300">
{service.title}
</h4>
<div className="open-description">
<p className="text-text group-hover:text-white/90 mb-5 transition-colors duration-300">
{service.description}
</p>
<a
href="#read"
className="text-secondary group-hover:text-white font-bold transition-colors duration-300"
>
Read More
</a>
</div>
</div>
))}
</div>
</div>
</section>
)
}

View File

@@ -0,0 +1,145 @@
import { useEffect, useRef, useState } from 'react'
import { statsData, serviceListData, commonDescriptions } from '../../data/siteData'
function useCountUp(end: number, duration: number = 2000, startCounting: boolean = false) {
const [count, setCount] = useState(0)
useEffect(() => {
if (!startCounting) return
let startTime: number
let animationFrame: number
const animate = (currentTime: number) => {
if (!startTime) startTime = currentTime
const progress = Math.min((currentTime - startTime) / duration, 1)
setCount(Math.floor(progress * end))
if (progress < 1) {
animationFrame = requestAnimationFrame(animate)
}
}
animationFrame = requestAnimationFrame(animate)
return () => cancelAnimationFrame(animationFrame)
}, [end, duration, startCounting])
return count
}
function StatItem({ value, label, startCounting }: { value: number; label: string; startCounting: boolean }) {
const count = useCountUp(value, 2000, startCounting)
return (
<div className="stats-1-left">
<h4 className="text-[36px] font-semibold mb-1.5 font-sans" style={{ color: '#ffffff' }}>{count}</h4>
<h6 className="text-lg" style={{ color: '#ffffff' }}>{label}</h6>
</div>
)
}
export default function Stats() {
const sectionRef = useRef<HTMLElement>(null)
const [startCounting, setStartCounting] = useState(false)
useEffect(() => {
const observer = new IntersectionObserver(
(entries) => {
if (entries[0].isIntersecting) {
setStartCounting(true)
observer.disconnect()
}
},
{ threshold: 0.3 }
)
if (sectionRef.current) {
observer.observe(sectionRef.current)
}
return () => observer.disconnect()
}, [])
return (
<section
ref={sectionRef}
className="py-16 relative"
id="stats"
style={{
backgroundImage: "url('/src/assets/images/2.jpg')",
backgroundSize: 'cover',
backgroundPosition: 'center',
}}
>
{/* Overlay */}
<div
className="absolute inset-0 z-0"
style={{ background: 'rgba(10, 30, 80, 0.92)' }}
/>
<div className="container mx-auto px-4 py-3 relative z-10">
{/* Section Header */}
<div className="text-center max-w-3xl mx-auto mb-8">
<h3 className="text-3xl md:text-4xl font-bold mb-4 font-sans" style={{ color: '#ffffff' }}>
{commonDescriptions.statsTitle}
</h3>
<p className="my-3" style={{ color: '#d0d0d0' }}>
{commonDescriptions.sectionDesc}
</p>
</div>
<div className="flex flex-wrap lg:flex-nowrap gap-12 pt-8 mt-3">
{/* Left Content */}
<div className="w-full lg:w-5/12">
<h4 className="text-[38px] leading-[46px] font-semibold mb-4 font-sans" style={{ color: '#ffffff' }}>
{commonDescriptions.statsSubtitle}
</h4>
<p className="mt-2.5 text-base leading-6" style={{ color: '#d0d0d0' }}>
{commonDescriptions.statsDesc}
</p>
<p className="mt-2.5 text-base leading-6" style={{ color: '#d0d0d0' }}>
{commonDescriptions.statsDesc2}
</p>
{/* Stats Grid */}
<div className="grid grid-cols-3 gap-2.5 mt-12">
{statsData.map((stat) => (
<StatItem
key={stat.id}
value={stat.value}
label={stat.label}
startCounting={startCounting}
/>
))}
</div>
</div>
{/* Right Content - Service List */}
<div className="w-full lg:w-7/12 my-8 lg:my-0">
<div className="grid grid-cols-1 md:grid-cols-2 gap-[30px]">
{serviceListData.map((service) => (
<div
key={service.id}
className="group stats-service-card flex gap-4 p-10 rounded"
>
<div className="flex-shrink-0">
<span className={`fa fa-${service.icon} text-[32px] text-secondary transition-colors duration-300`} />
</div>
<div>
<h6 className="font-bold mb-2.5 service-title">
<a href="#url" className="text-white group-hover:text-title transition-colors duration-300">
{service.title}
</a>
</h6>
<p className="text-[15px] leading-[25px] mt-2.5 text-[#d0d0d0] group-hover:text-title transition-colors duration-300">
{service.description}
</p>
</div>
</div>
))}
</div>
</div>
</div>
</div>
</section>
)
}

View File

@@ -0,0 +1,84 @@
import { useState } from 'react'
import { testimonialsData, commonDescriptions } from '../../data/siteData'
export default function Testimonials() {
const [activeSlide, setActiveSlide] = useState(0)
// 创建两组轮播数据(与原始 HTML 一致)
const slides = [
[testimonialsData[0], testimonialsData[1], testimonialsData[2]],
[testimonialsData[1], testimonialsData[2], testimonialsData[0]],
]
return (
<section className="py-20 bg-white" id="testimonials">
<div className="container mx-auto px-4">
{/* Section Header */}
<div className="text-center max-w-3xl mx-auto mb-8">
<h3 className="text-3xl md:text-4xl font-bold text-title mb-4 font-sans">
{commonDescriptions.testimonialsTitle}
</h3>
<p className="text-text my-3">
{commonDescriptions.sectionDesc}
</p>
</div>
{/* Carousel */}
<div className="relative pb-12">
{/* Slides */}
<div className="overflow-hidden">
<div
className="flex transition-transform duration-500 ease-in-out"
style={{ transform: `translateX(-${activeSlide * 100}%)` }}
>
{slides.map((slideGroup, slideIndex) => (
<div
key={slideIndex}
className="w-full flex-shrink-0"
>
<div className="grid md:grid-cols-3 gap-6 py-8 mt-3">
{slideGroup.map((testimonial, index) => (
<div
key={`${slideIndex}-${index}`}
className={`bg-white rounded-lg shadow-card p-6 ${
index === 2 ? 'md:col-start-2 lg:col-start-auto' : ''
}`}
>
<div className="text-center">
<img
src={testimonial.image}
alt={testimonial.name}
className="w-20 h-20 rounded-full mx-auto object-cover"
/>
<h3 className="text-xl font-bold text-title mt-2 font-sans">
{testimonial.name}
</h3>
<p className="text-secondary mb-3">{testimonial.role}</p>
<p className="text-text">
{testimonial.content}
</p>
</div>
</div>
))}
</div>
</div>
))}
</div>
</div>
{/* Indicators */}
<div className="carousel-indicators">
{slides.map((_, index) => (
<button
key={index}
onClick={() => setActiveSlide(index)}
className={activeSlide === index ? 'active' : ''}
aria-label={`Go to slide ${index + 1}`}
/>
))}
</div>
</div>
</div>
</section>
)
}

View File

@@ -0,0 +1,149 @@
import { Link } from '@tanstack/react-router'
import { contactInfo, footerLinks, socialLinks } from '../../data/siteData'
// 社交链接hover颜色映射
const socialHoverColors: Record<string, string> = {
facebook: 'hover:bg-[#3b5998]',
twitter: 'hover:bg-[#1da1f2]',
instagram: 'hover:bg-[#c13584]',
'google-plus': 'hover:bg-[#dd4b39]',
linkedin: 'hover:bg-[#0077b5]',
}
export default function Footer() {
return (
<footer
className="relative text-white"
style={{
backgroundImage: "url('/src/assets/images/5.jpg')",
backgroundSize: 'cover',
backgroundPosition: 'center',
}}
>
{/* Overlay */}
<div
className="absolute inset-0 z-0"
style={{ background: 'rgba(20, 20, 20, 0.94)' }}
/>
<div className="container mx-auto px-4 py-16 relative z-10">
{/* Footer Top - 使用原始的 2fr 1fr 2fr 1fr 布局 */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-[2fr_1fr_2fr_1fr] gap-10 mb-12">
{/* Contact Us */}
<div>
<h6 className="text-xl font-semibold mb-6 font-sans" style={{ color: '#ffffff' }}>Contact Us</h6>
<ul className="space-y-2.5">
<li className="flex items-start gap-3">
<span className="fa fa-map-marker text-secondary w-5 flex-shrink-0" />
<p className="text-white text-base leading-[25px]">{contactInfo.address}</p>
</li>
<li>
<a href={`tel:${contactInfo.phone}`} className="flex items-center gap-3 text-white hover:text-secondary transition-colors">
<span className="fa fa-phone text-secondary w-5" />
<span>{contactInfo.phone}</span>
</a>
</li>
<li>
<a href={`mailto:${contactInfo.email}`} className="flex items-center gap-3 text-white hover:text-secondary transition-colors">
<span className="fa fa-envelope-open-o text-secondary w-5" />
<span>{contactInfo.email}</span>
</a>
</li>
</ul>
{/* Social Links */}
<div className="flex gap-2.5 mt-5">
{socialLinks.map((social) => (
<a
key={social.name}
href={social.href}
className={`w-[35px] h-[35px] rounded-full bg-white/10 flex items-center justify-center transition-colors ${socialHoverColors[social.icon] || 'hover:bg-secondary'}`}
aria-label={social.name}
>
<span className={`fa fa-${social.icon} leading-[35px]`} aria-hidden="true" />
</a>
))}
</div>
</div>
{/* Featured Links */}
<div>
<h6 className="text-xl font-semibold mb-6 font-sans" style={{ color: '#ffffff' }}>Featured Links</h6>
<ul className="space-y-2.5">
{footerLinks.featured.map((link) => (
<li key={link.name}>
<Link
to={link.href}
className="text-white text-base leading-[25px] hover:text-secondary transition-colors"
>
{link.name}
</Link>
</li>
))}
</ul>
</div>
{/* Newsletter */}
<div>
<h6 className="text-xl font-semibold mb-6 font-sans" style={{ color: '#ffffff' }}>Newsletter</h6>
<p className="text-white mb-3">Get in your inbox the latest News and</p>
<form className="flex mb-6" onSubmit={(e) => e.preventDefault()}>
<input
type="email"
placeholder="Email"
required
className="flex-1 bg-white/10 border border-white/15 px-5 py-3 text-white rounded-l outline-none"
/>
<button
type="submit"
className="bg-secondary px-5 py-3 rounded-r hover:bg-secondary/80 transition-colors"
>
<span className="fa fa-envelope-o" aria-hidden="true" />
</button>
</form>
<p className="text-white">Subscribe and get our weekly newsletter</p>
<p className="text-white">We'll never share your email address</p>
</div>
{/* Quick Links */}
<div>
<h6 className="text-xl font-semibold mb-6 font-sans" style={{ color: '#ffffff' }}>Quick Links</h6>
<ul className="space-y-2.5">
{footerLinks.quick.map((link) => (
<li key={link.name}>
<Link
to={link.href}
className="text-white text-base leading-[25px] hover:text-secondary transition-colors"
>
{link.name}
</Link>
</li>
))}
</ul>
</div>
</div>
{/* Footer Bottom */}
<div className="border-t border-[#454545] pt-8 mt-8">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<p className="text-white text-base leading-[25px]">
&copy; 2025 Finance Ideas. All rights reserved.
</p>
<ul className="flex gap-4 md:justify-end">
<li>
<a href="#" className="text-white hover:text-secondary transition-colors">
Privacy policy
</a>
</li>
<li>
<a href="#" className="text-white hover:text-secondary transition-colors">
Terms of service
</a>
</li>
</ul>
</div>
</div>
</div>
</footer>
)
}

View File

@@ -0,0 +1,143 @@
import { useState, useEffect } from 'react'
import { Link, useLocation } from '@tanstack/react-router'
import { menuItems } from '../../data/siteData'
export default function Header() {
const [isMenuOpen, setIsMenuOpen] = useState(false)
const [isScrolled, setIsScrolled] = useState(false)
const [isSearchOpen, setIsSearchOpen] = useState(false)
const location = useLocation()
useEffect(() => {
const handleScroll = () => {
setIsScrolled(window.scrollY > 80)
}
window.addEventListener('scroll', handleScroll)
return () => window.removeEventListener('scroll', handleScroll)
}, [])
// 关闭菜单当路由变化时
useEffect(() => {
setIsMenuOpen(false)
}, [location.pathname])
return (
<header className={`fixed top-0 left-0 right-0 z-50 transition-all duration-300 ${
isScrolled ? 'bg-white shadow-md' : 'bg-transparent'
}`}>
<nav className="container mx-auto px-4">
<div className="flex items-center justify-between py-4">
{/* Logo */}
<Link to="/" className="flex items-center">
<span className={`text-2xl font-bold font-sans transition-colors ${
isScrolled ? 'text-primary' : 'text-white'
}`}>
Finance <span className="text-secondary">Ideas</span>
</span>
</Link>
{/* Desktop Navigation */}
<div className="hidden lg:flex items-center gap-8">
<ul className="flex items-center gap-6">
{menuItems.map((item) => (
<li key={item.name}>
<Link
to={item.href}
className={`font-medium transition-colors hover:text-secondary ${
isScrolled ? 'text-title' : 'text-white'
} ${location.pathname === item.href ? 'text-secondary' : ''}`}
>
{item.name}
</Link>
</li>
))}
</ul>
{/* Search Button */}
<button
onClick={() => setIsSearchOpen(true)}
className={`p-2 transition-colors hover:text-secondary ${
isScrolled ? 'text-title' : 'text-white'
}`}
aria-label="Search"
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
</svg>
</button>
</div>
{/* Mobile Menu Button */}
<button
onClick={() => setIsMenuOpen(!isMenuOpen)}
className={`lg:hidden p-2 transition-colors ${
isScrolled ? 'text-title' : 'text-white'
}`}
aria-label="Toggle menu"
>
{isMenuOpen ? (
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
) : (
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 12h16M4 18h16" />
</svg>
)}
</button>
</div>
{/* Mobile Navigation */}
<div className={`lg:hidden overflow-hidden transition-all duration-300 ${
isMenuOpen ? 'max-h-64 pb-4' : 'max-h-0'
}`}>
<ul className="flex flex-col gap-4">
{menuItems.map((item) => (
<li key={item.name}>
<Link
to={item.href}
className={`block font-medium transition-colors hover:text-secondary ${
isScrolled ? 'text-title' : 'text-white'
} ${location.pathname === item.href ? 'text-secondary' : ''}`}
>
{item.name}
</Link>
</li>
))}
</ul>
</div>
</nav>
{/* Search Popup */}
{isSearchOpen && (
<div className="fixed inset-0 bg-black/80 flex items-center justify-center z-50">
<div className="w-full max-w-2xl px-4">
<form className="relative" onSubmit={(e) => e.preventDefault()}>
<input
type="search"
placeholder="Search your Keyword"
className="w-full h-16 px-6 pr-14 text-lg rounded-lg border-0 focus:ring-2 focus:ring-primary"
autoFocus
/>
<button
type="submit"
className="absolute right-4 top-1/2 -translate-y-1/2 text-text hover:text-primary"
>
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
</svg>
</button>
</form>
</div>
<button
onClick={() => setIsSearchOpen(false)}
className="absolute top-8 right-8 text-white text-4xl hover:text-secondary"
aria-label="Close search"
>
&times;
</button>
</div>
)}
</header>
)
}

View File

@@ -0,0 +1,32 @@
import { useState, useEffect } from 'react'
export default function ScrollToTop() {
const [isVisible, setIsVisible] = useState(false)
useEffect(() => {
const handleScroll = () => {
setIsVisible(window.scrollY > 200)
}
window.addEventListener('scroll', handleScroll)
return () => window.removeEventListener('scroll', handleScroll)
}, [])
const scrollToTop = () => {
window.scrollTo({
top: 0,
behavior: 'smooth',
})
}
return (
<button
onClick={scrollToTop}
className={`fixed bottom-4 right-4 w-10 h-10 bg-secondary hover:bg-secondary/80 text-white rounded shadow-lg flex items-center justify-center transition-all duration-300 z-50 ${
isVisible ? 'opacity-100 translate-y-0' : 'opacity-0 translate-y-4 pointer-events-none'
}`}
aria-label="Back to top"
>
<span className="fa fa-angle-up" />
</button>
)
}

View File

@@ -0,0 +1,47 @@
import { advanceFeaturesData, commonDescriptions } from '../../data/siteData'
export default function AdvanceFeatures() {
return (
<section className="py-20 bg-light-bg" id="features">
<div className="container mx-auto px-4">
{/* Section Header */}
<div className="text-center max-w-3xl mx-auto mb-12">
<h3 className="text-3xl md:text-4xl font-bold text-title mb-4 font-sans">
{commonDescriptions.advanceFeaturesTitle}
</h3>
<p className="text-text my-3">
{commonDescriptions.sectionDesc}
</p>
</div>
{/* Features Grid */}
<div className="grid md:grid-cols-2 gap-8 mt-8 pt-3">
{advanceFeaturesData.map((feature) => (
<div
key={feature.id}
className="feature-gd grid gap-5"
style={{ gridTemplateColumns: 'auto 1fr' }}
>
<div className="icon flex-shrink-0 w-[55px] h-[55px] bg-secondary rounded text-center leading-[55px]">
<span className={`fa fa-${feature.icon} text-[22px] text-white`} aria-hidden="true" />
</div>
<div className="icon-info">
<h5 className="text-[20px] leading-[30px] font-bold mb-3 font-sans">
<a href="#" className="text-title hover:text-secondary transition-colors">
{feature.title}
</a>
</h5>
<p className="text-text text-base leading-6 mb-3 max-w-[450px]">
{feature.description}
</p>
<a href="#" className="text-secondary hover:text-primary transition-colors font-bold">
Read More <span className="fa fa-angle-right pl-1" />
</a>
</div>
</div>
))}
</div>
</div>
</section>
)
}

View File

@@ -0,0 +1,52 @@
import { processStepsData, commonDescriptions } from '../../data/siteData'
export default function ProcessSteps() {
return (
<section
className="relative"
id="process"
style={{
backgroundImage: "url('/src/assets/images/2.jpg')",
backgroundSize: 'cover',
backgroundPosition: 'center',
}}
>
{/* Overlay */}
<div
className="absolute inset-0 z-0"
style={{ background: 'linear-gradient(45deg, #17449e, rgba(0, 0, 0, 0.8))', opacity: 0.9 }}
/>
<div className="container mx-auto px-4 py-24 relative z-10">
{/* Section Header */}
<div className="text-center mx-auto">
<h3 className="text-3xl md:text-4xl font-bold mb-4 font-sans" style={{ color: '#ffffff' }}>
{commonDescriptions.processTitle}
</h3>
<p className="text-white my-3">
{commonDescriptions.sectionDesc}
</p>
</div>
{/* Steps */}
<div className="grid sm:grid-cols-2 lg:grid-cols-4 gap-8 mt-5 pt-3 text-center">
{processStepsData.map((step) => (
<div key={step.id} className="three-grids-columns">
<div className="icon mb-4">
<span className="inline-flex items-center justify-center w-[50px] h-[50px] rounded-full bg-secondary text-white text-2xl">
{step.number}
</span>
</div>
<h4 className="text-[22px] font-semibold mt-5 mb-4 font-sans" style={{ color: '#ffffff' }}>
{step.title}
</h4>
<p className="text-white/70 leading-6">
{step.description}
</p>
</div>
))}
</div>
</div>
</section>
)
}

View File

@@ -0,0 +1,29 @@
import { serviceCardsData } from '../../data/siteData'
export default function ServiceCards() {
return (
<section className="py-20 bg-white" id="services">
<div className="container mx-auto px-4">
<div className="grid md:grid-cols-3 gap-6">
{serviceCardsData.map((service) => (
<div
key={service.id}
className={`${service.bgClass} min-h-[280px] rounded-lg overflow-hidden`}
>
<div className="p-8 md:p-12 h-full flex flex-col justify-center text-center">
<h4 className="text-xl font-bold mb-4 font-sans">
<a href="#url" style={{ color: '#ffffff' }} className="hover:text-secondary transition-colors">
{service.title}
</a>
</h4>
<p className="text-white/90">
{service.description}
</p>
</div>
</div>
))}
</div>
</div>
</section>
)
}

View File

@@ -0,0 +1,25 @@
import { Link } from '@tanstack/react-router'
interface BreadcrumbProps {
title: string
currentPage?: string
}
export default function Breadcrumb({ title, currentPage }: BreadcrumbProps) {
return (
<section className="breadcrum-bg py-20">
<div className="container mx-auto px-4 py-5">
<h2 className="text-3xl md:text-4xl font-bold mb-2 font-sans" style={{ color: '#ffffff' }}>
{title}
</h2>
<p className="text-white">
<Link to="/" className="hover:text-secondary transition-colors">
Home
</Link>
&nbsp; / &nbsp;
{currentPage || title}
</p>
</div>
</section>
)
}