first commit
This commit is contained in:
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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user