feat: replace all marketing home components with reference landing design
- Extract landing.css (scoped under .marketing-page) from texpixel-landing.html - Add Lora + JetBrains Mono fonts to index.html - Update MarketingLayout with .marketing-page wrapper and glow blobs - Replace MarketingNavbar with reference design (auth-aware user menu) - Replace HeroSection with mock window + cycling LaTeX typing effect - Replace FeaturesSection, PricingSection, Footer with reference designs - Add ProductSuiteSection, ShowcaseSection, TestimonialsSection (carousel), DocsSeoSection - Add useScrollReveal hook for intersection-based fade-in animations - Update HomePage to wire all sections in correct order - Remove obsolete HowItWorksSection and ContactSection - Remove dead contact key from marketing.nav translations Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,162 +1,141 @@
|
||||
import { useState, useRef, useEffect } from 'react';
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
import { Link, useLocation } from 'react-router-dom';
|
||||
import { Languages, ChevronDown, Check, Menu, X } from 'lucide-react';
|
||||
import { useLanguage } from '../../contexts/LanguageContext';
|
||||
import { useAuth } from '../../contexts/AuthContext';
|
||||
|
||||
export default function MarketingNavbar() {
|
||||
const { language, setLanguage, t } = useLanguage();
|
||||
const { language, setLanguage } = useLanguage();
|
||||
const { user, signOut } = useAuth();
|
||||
const location = useLocation();
|
||||
const [showLangMenu, setShowLangMenu] = useState(false);
|
||||
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
|
||||
const [scrolled, setScrolled] = useState(false);
|
||||
const langMenuRef = useRef<HTMLDivElement>(null);
|
||||
const isHome = location.pathname === '/';
|
||||
const [scrolled, setScrolled] = useState(false);
|
||||
const [activeSection, setActiveSection] = useState('');
|
||||
const [userMenuOpen, setUserMenuOpen] = useState(false);
|
||||
const userMenuRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// Scroll: sticky style + active nav section
|
||||
useEffect(() => {
|
||||
const handleScroll = () => {
|
||||
setScrolled(window.scrollY > 10);
|
||||
if (!isHome) return;
|
||||
let current = '';
|
||||
document.querySelectorAll('section[id]').forEach((s) => {
|
||||
if (window.scrollY >= (s as HTMLElement).offsetTop - 100) {
|
||||
current = s.id;
|
||||
}
|
||||
});
|
||||
setActiveSection(current);
|
||||
};
|
||||
window.addEventListener('scroll', handleScroll, { passive: true });
|
||||
return () => window.removeEventListener('scroll', handleScroll);
|
||||
}, [isHome]);
|
||||
|
||||
// Close user menu on outside click
|
||||
useEffect(() => {
|
||||
const handle = (e: MouseEvent) => {
|
||||
if (userMenuRef.current && !userMenuRef.current.contains(e.target as Node)) {
|
||||
setUserMenuOpen(false);
|
||||
}
|
||||
};
|
||||
document.addEventListener('mousedown', handle);
|
||||
return () => document.removeEventListener('mousedown', handle);
|
||||
}, []);
|
||||
|
||||
const navLinks = [
|
||||
{ to: '/', label: t.marketing.nav.home },
|
||||
{ to: '/docs', label: t.marketing.nav.docs },
|
||||
{ to: '/blog', label: t.marketing.nav.blog },
|
||||
{ href: '/', label: language === 'zh' ? '首页' : 'Home' },
|
||||
{ href: '/docs', label: language === 'zh' ? '文档' : 'Docs' },
|
||||
{ href: '/blog', label: language === 'zh' ? '博客' : 'Blog' },
|
||||
];
|
||||
|
||||
const anchorLinks = isHome
|
||||
? [
|
||||
{ href: '#pricing', label: t.marketing.nav.pricing },
|
||||
{ href: '#contact', label: t.marketing.nav.contact },
|
||||
]
|
||||
? [{ href: '#pricing', label: language === 'zh' ? '价格' : 'Pricing' }]
|
||||
: [];
|
||||
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
if (langMenuRef.current && !langMenuRef.current.contains(event.target as Node)) {
|
||||
setShowLangMenu(false);
|
||||
}
|
||||
};
|
||||
document.addEventListener('mousedown', handleClickOutside);
|
||||
return () => document.removeEventListener('mousedown', handleClickOutside);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const handleScroll = () => setScrolled(window.scrollY > 10);
|
||||
window.addEventListener('scroll', handleScroll, { passive: true });
|
||||
return () => window.removeEventListener('scroll', handleScroll);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<nav
|
||||
className={`h-16 flex items-center justify-between px-6 flex-shrink-0 z-[60] relative transition-all duration-300 ${
|
||||
scrolled
|
||||
? 'bg-white/90 backdrop-blur-md border-b border-cream-300 shadow-sm'
|
||||
: 'bg-transparent border-b border-transparent'
|
||||
}`}
|
||||
>
|
||||
<Link to="/" className="flex items-center gap-2.5 group">
|
||||
<img src="/texpixel-app-icon.svg" alt="TexPixel" className="w-8 h-8 transition-transform group-hover:scale-110 duration-200" />
|
||||
<span className="text-xl font-display font-bold text-ink tracking-tight">TexPixel</span>
|
||||
</Link>
|
||||
<nav style={{ opacity: scrolled ? 1 : undefined }}>
|
||||
<div className="nav-inner">
|
||||
<Link to="/" className="nav-logo">
|
||||
<div className="logo-icon">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="white" strokeWidth="2">
|
||||
<path d="M4 6h16M4 10h10M4 14h12M4 18h8" />
|
||||
</svg>
|
||||
</div>
|
||||
<span style={{ fontFamily: "'Lora', serif" }}>TexPixel</span>
|
||||
</Link>
|
||||
|
||||
<div className="hidden md:flex items-center gap-1">
|
||||
{navLinks.map((link) => {
|
||||
const isActive = location.pathname === link.to;
|
||||
return (
|
||||
<Link
|
||||
key={link.to}
|
||||
to={link.to}
|
||||
className={`relative px-4 py-2 text-sm font-medium rounded-lg transition-colors duration-200 ${
|
||||
isActive
|
||||
? 'text-coral-600 bg-coral-50'
|
||||
: 'text-ink-secondary hover:text-ink hover:bg-cream-200/60'
|
||||
}`}
|
||||
>
|
||||
{link.label}
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
{anchorLinks.map((link) => (
|
||||
<a
|
||||
key={link.href}
|
||||
href={link.href}
|
||||
className="px-4 py-2 text-sm font-medium text-ink-secondary hover:text-ink hover:bg-cream-200/60 rounded-lg transition-colors duration-200"
|
||||
>
|
||||
{link.label}
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
<ul className="nav-links">
|
||||
{navLinks.map((link) => (
|
||||
<li key={link.href}>
|
||||
<Link
|
||||
to={link.href}
|
||||
className={location.pathname === link.href ? 'active' : ''}
|
||||
>
|
||||
{link.label}
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
{anchorLinks.map((link) => (
|
||||
<li key={link.href}>
|
||||
<a
|
||||
href={link.href}
|
||||
className={activeSection === link.href.slice(1) ? 'active' : ''}
|
||||
>
|
||||
{link.label}
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
{/* Language Switcher */}
|
||||
<div className="relative" ref={langMenuRef}>
|
||||
<div className="nav-right">
|
||||
<button
|
||||
onClick={() => setShowLangMenu(!showLangMenu)}
|
||||
className="flex items-center gap-1.5 px-3 py-2 hover:bg-cream-200/60 rounded-lg text-ink-secondary text-sm font-medium transition-colors"
|
||||
className="lang-switch"
|
||||
onClick={() => setLanguage(language === 'zh' ? 'en' : 'zh')}
|
||||
>
|
||||
<Languages size={16} />
|
||||
<span className="hidden sm:inline">{language === 'en' ? 'EN' : '中文'}</span>
|
||||
<ChevronDown size={12} className={`transition-transform duration-200 ${showLangMenu ? 'rotate-180' : ''}`} />
|
||||
{language === 'zh' ? 'EN' : '中文'}
|
||||
</button>
|
||||
{showLangMenu && (
|
||||
<div className="absolute right-0 top-full mt-2 w-36 bg-white rounded-xl shadow-lg border border-cream-300 py-1.5 z-50">
|
||||
{(['en', 'zh'] as const).map((lang) => (
|
||||
<button
|
||||
key={lang}
|
||||
onClick={() => { setLanguage(lang); setShowLangMenu(false); }}
|
||||
className={`w-full flex items-center justify-between px-4 py-2.5 text-sm transition-colors hover:bg-cream-100 ${
|
||||
language === lang ? 'text-coral-600 font-semibold' : 'text-ink-secondary'
|
||||
}`}
|
||||
>
|
||||
{lang === 'en' ? 'English' : '简体中文'}
|
||||
{language === lang && <Check size={14} />}
|
||||
</button>
|
||||
))}
|
||||
|
||||
{user ? (
|
||||
<div className="nav-user" ref={userMenuRef} style={{ position: 'relative' }}>
|
||||
<div
|
||||
className="nav-avatar"
|
||||
onClick={() => setUserMenuOpen((o) => !o)}
|
||||
style={{ cursor: 'pointer' }}
|
||||
>
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8">
|
||||
<circle cx="12" cy="8" r="4" />
|
||||
<path d="M4 20c0-4 3.6-7 8-7s8 3 8 7" />
|
||||
</svg>
|
||||
</div>
|
||||
{userMenuOpen && (
|
||||
<div className="nav-user-menu" style={{ display: 'block' }}>
|
||||
<div className="nav-menu-divider" />
|
||||
<Link to="/app" className="nav-menu-item" onClick={() => setUserMenuOpen(false)}>
|
||||
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8">
|
||||
<rect x="2" y="3" width="20" height="14" rx="2" />
|
||||
<path d="M8 21h8M12 17v4" />
|
||||
</svg>
|
||||
{language === 'zh' ? '启动应用' : 'Launch App'}
|
||||
</Link>
|
||||
<div className="nav-menu-divider" />
|
||||
<button
|
||||
className="nav-menu-item nav-menu-logout"
|
||||
onClick={() => { signOut(); setUserMenuOpen(false); }}
|
||||
style={{ background: 'none', border: 'none', width: '100%', textAlign: 'left', cursor: 'pointer' }}
|
||||
>
|
||||
{language === 'zh' ? '退出登录' : 'Sign Out'}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="nav-login-btn">
|
||||
<Link to="/app" className="btn-cta" style={{ display: 'inline-block', lineHeight: '52px', padding: '0 24px', textDecoration: 'none' }}>
|
||||
{language === 'zh' ? '免费试用' : 'Try Free'}
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Link
|
||||
to="/app"
|
||||
className="hidden sm:inline-flex items-center px-5 py-2 btn-primary text-sm rounded-lg"
|
||||
>
|
||||
{t.marketing.nav.launchApp}
|
||||
</Link>
|
||||
|
||||
<button className="md:hidden p-2 text-ink-secondary hover:text-ink" onClick={() => setMobileMenuOpen(!mobileMenuOpen)}>
|
||||
{mobileMenuOpen ? <X size={22} /> : <Menu size={22} />}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Mobile menu */}
|
||||
{mobileMenuOpen && (
|
||||
<div className="absolute top-16 left-0 right-0 bg-white/95 backdrop-blur-md border-b border-cream-300 shadow-lg md:hidden z-50 py-3 px-6 flex flex-col gap-1">
|
||||
{navLinks.map((link) => (
|
||||
<Link
|
||||
key={link.to}
|
||||
to={link.to}
|
||||
className={`text-sm font-medium py-2.5 px-3 rounded-lg transition-colors ${
|
||||
location.pathname === link.to ? 'text-coral-600 bg-coral-50' : 'text-ink-secondary hover:bg-cream-200/60'
|
||||
}`}
|
||||
onClick={() => setMobileMenuOpen(false)}
|
||||
>
|
||||
{link.label}
|
||||
</Link>
|
||||
))}
|
||||
{anchorLinks.map((link) => (
|
||||
<a
|
||||
key={link.href}
|
||||
href={link.href}
|
||||
className="text-sm font-medium text-ink-secondary py-2.5 px-3 rounded-lg hover:bg-cream-200/60"
|
||||
onClick={() => setMobileMenuOpen(false)}
|
||||
>
|
||||
{link.label}
|
||||
</a>
|
||||
))}
|
||||
<Link
|
||||
to="/app"
|
||||
className="text-sm font-semibold text-coral-600 py-2.5 px-3"
|
||||
onClick={() => setMobileMenuOpen(false)}
|
||||
>
|
||||
{t.marketing.nav.launchApp}
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user