first commit
This commit is contained in:
42
src/components/about/Mission.tsx
Normal file
42
src/components/about/Mission.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
70
src/components/about/Statistics.tsx
Normal file
70
src/components/about/Statistics.tsx
Normal 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()}</>
|
||||
}
|
||||
63
src/components/about/Team.tsx
Normal file
63
src/components/about/Team.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
39
src/components/about/WhyChooseUs.tsx
Normal file
39
src/components/about/WhyChooseUs.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
120
src/components/contact/ContactForm.tsx
Normal file
120
src/components/contact/ContactForm.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
47
src/components/contact/Map.tsx
Normal file
47
src/components/contact/Map.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
36
src/components/home/CTA.tsx
Normal file
36
src/components/home/CTA.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
51
src/components/home/Features.tsx
Normal file
51
src/components/home/Features.tsx
Normal 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 →
|
||||
</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>
|
||||
)
|
||||
}
|
||||
70
src/components/home/HeroSlider.tsx
Normal file
70
src/components/home/HeroSlider.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
65
src/components/home/LatestNews.tsx
Normal file
65
src/components/home/LatestNews.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
51
src/components/home/Services.tsx
Normal file
51
src/components/home/Services.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
145
src/components/home/Stats.tsx
Normal file
145
src/components/home/Stats.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
84
src/components/home/Testimonials.tsx
Normal file
84
src/components/home/Testimonials.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
149
src/components/layout/Footer.tsx
Normal file
149
src/components/layout/Footer.tsx
Normal 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]">
|
||||
© 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>
|
||||
)
|
||||
}
|
||||
143
src/components/layout/Header.tsx
Normal file
143
src/components/layout/Header.tsx
Normal 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"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</header>
|
||||
)
|
||||
}
|
||||
32
src/components/layout/ScrollToTop.tsx
Normal file
32
src/components/layout/ScrollToTop.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
47
src/components/services/AdvanceFeatures.tsx
Normal file
47
src/components/services/AdvanceFeatures.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
52
src/components/services/ProcessSteps.tsx
Normal file
52
src/components/services/ProcessSteps.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
29
src/components/services/ServiceCards.tsx
Normal file
29
src/components/services/ServiceCards.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
25
src/components/shared/Breadcrumb.tsx
Normal file
25
src/components/shared/Breadcrumb.tsx
Normal 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>
|
||||
/
|
||||
{currentPage || title}
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user