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:
@@ -12,7 +12,7 @@
|
||||
<!-- Fonts: Plus Jakarta Sans (headings) + DM Sans (body) -->
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@500;600;700;800&family=DM+Sans:ital,wght@0,400;0,500;0,600;1,400&display=swap" rel="stylesheet" />
|
||||
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@500;600;700;800&family=DM+Sans:ital,opsz,wght@0,9..40,300;0,9..40,400;0,9..40,500;0,9..40,600;1,9..40,300&family=Lora:ital,wght@0,400;0,600;0,700;1,400&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet" />
|
||||
|
||||
<!-- hreflang: same URL serves both languages (SPA), point both to canonical -->
|
||||
<link rel="canonical" href="https://texpixel.com/" />
|
||||
|
||||
@@ -1,99 +0,0 @@
|
||||
import { useState } from 'react';
|
||||
import { Mail, Users, Send, ArrowUpRight } from 'lucide-react';
|
||||
import { useLanguage } from '../../contexts/LanguageContext';
|
||||
|
||||
export default function ContactSection() {
|
||||
const { t } = useLanguage();
|
||||
const c = t.marketing.contact;
|
||||
const [status, setStatus] = useState<'idle' | 'sending' | 'sent'>('idle');
|
||||
|
||||
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
setStatus('sending');
|
||||
setTimeout(() => {
|
||||
setStatus('sent');
|
||||
setTimeout(() => setStatus('idle'), 3000);
|
||||
(e.target as HTMLFormElement).reset();
|
||||
}, 1000);
|
||||
};
|
||||
|
||||
return (
|
||||
<section id="contact" className="section-padding relative" style={{ backgroundColor: 'var(--color-bg)' }}>
|
||||
<div className="max-w-6xl mx-auto">
|
||||
<div className="text-center mb-16">
|
||||
<h2 className="font-display text-3xl lg:text-4xl font-bold text-ink mb-4 tracking-tight">{c.title}</h2>
|
||||
<p className="text-ink-secondary text-lg">{c.subtitle}</p>
|
||||
</div>
|
||||
|
||||
<div className="grid md:grid-cols-5 gap-10 max-w-4xl mx-auto">
|
||||
{/* Contact info — 2 cols */}
|
||||
<div className="md:col-span-2 space-y-5">
|
||||
<a
|
||||
href="mailto:yogecoder@gmail.com"
|
||||
className="card p-5 flex items-start gap-4 group hover:border-coral-200 block"
|
||||
>
|
||||
<div className="w-10 h-10 bg-coral-50 rounded-xl flex items-center justify-center flex-shrink-0 group-hover:scale-110 transition-transform">
|
||||
<Mail size={18} className="text-coral-500" />
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="text-xs text-ink-muted mb-1 font-medium">{t.common.email}</div>
|
||||
<div className="text-sm font-medium text-ink truncate">yogecoder@gmail.com</div>
|
||||
</div>
|
||||
<ArrowUpRight size={14} className="text-ink-muted group-hover:text-coral-500 transition-colors mt-1" />
|
||||
</a>
|
||||
|
||||
<div className="card p-5 flex items-start gap-4">
|
||||
<div className="w-10 h-10 bg-sage-50 rounded-xl flex items-center justify-center flex-shrink-0">
|
||||
<Users size={18} className="text-sage-600" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-xs text-ink-muted mb-1 font-medium">{c.qqGroup}</div>
|
||||
<span className="text-sm font-medium text-ink font-mono">1018282100</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Contact form — 3 cols */}
|
||||
<form onSubmit={handleSubmit} className="md:col-span-3 space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-ink mb-1.5">{c.nameLabel}</label>
|
||||
<input
|
||||
type="text"
|
||||
required
|
||||
className="w-full px-4 py-2.5 bg-white border border-cream-300 rounded-xl focus:ring-2 focus:ring-coral-200 focus:border-coral-400 outline-none transition-all text-sm"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-ink mb-1.5">{c.emailLabel}</label>
|
||||
<input
|
||||
type="email"
|
||||
required
|
||||
className="w-full px-4 py-2.5 bg-white border border-cream-300 rounded-xl focus:ring-2 focus:ring-coral-200 focus:border-coral-400 outline-none transition-all text-sm"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-ink mb-1.5">{c.messageLabel}</label>
|
||||
<textarea
|
||||
required
|
||||
rows={4}
|
||||
className="w-full px-4 py-2.5 bg-white border border-cream-300 rounded-xl focus:ring-2 focus:ring-coral-200 focus:border-coral-400 outline-none transition-all text-sm resize-none"
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={status === 'sending'}
|
||||
className={`w-full py-3 rounded-xl text-sm font-semibold flex items-center justify-center gap-2 transition-all duration-200 ${
|
||||
status === 'sent'
|
||||
? 'bg-sage-500 text-white'
|
||||
: 'btn-primary justify-center'
|
||||
} disabled:opacity-60`}
|
||||
>
|
||||
<Send size={15} />
|
||||
{status === 'sending' ? c.sending : status === 'sent' ? c.sent : c.send}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
93
src/components/home/DocsSeoSection.tsx
Normal file
93
src/components/home/DocsSeoSection.tsx
Normal file
@@ -0,0 +1,93 @@
|
||||
import { Link } from 'react-router-dom';
|
||||
import { useLanguage } from '../../contexts/LanguageContext';
|
||||
|
||||
const GUIDES = [
|
||||
{
|
||||
svgPaths: (
|
||||
<>
|
||||
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/>
|
||||
<polyline points="14 2 14 8 20 8"/>
|
||||
</>
|
||||
),
|
||||
titleEn: 'How to convert image to LaTeX',
|
||||
titleZh: '图片转 LaTeX 完整指南',
|
||||
metaEn: '5 min read · Most popular',
|
||||
metaZh: '5 分钟 · 最受欢迎',
|
||||
},
|
||||
{
|
||||
svgPaths: (
|
||||
<>
|
||||
<rect x="4" y="4" width="16" height="16" rx="2"/>
|
||||
<path d="M8 9h8M8 12h6M8 15h4"/>
|
||||
</>
|
||||
),
|
||||
titleEn: 'Copy formula to Word — native equation format',
|
||||
titleZh: '复制公式到 Word — 原生公式格式',
|
||||
metaEn: '4 min read · Extension users',
|
||||
metaZh: '4 分钟 · 扩展用户',
|
||||
},
|
||||
{
|
||||
svgPaths: (
|
||||
<>
|
||||
<circle cx="11" cy="11" r="8"/>
|
||||
<path d="m21 21-4.35-4.35"/>
|
||||
</>
|
||||
),
|
||||
titleEn: 'OCR math formula guide — accuracy tips',
|
||||
titleZh: 'OCR 数学公式指南 — 提高准确度',
|
||||
metaEn: '6 min read · Power users',
|
||||
metaZh: '6 分钟 · 进阶用户',
|
||||
},
|
||||
{
|
||||
svgPaths: (
|
||||
<>
|
||||
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/>
|
||||
<path d="M14 2v6h6M10 12l2 2 4-4"/>
|
||||
</>
|
||||
),
|
||||
titleEn: 'PDF formula extraction — batch workflow',
|
||||
titleZh: 'PDF 公式批量提取工作流',
|
||||
metaEn: '8 min read · Desktop users',
|
||||
metaZh: '8 分钟 · 桌面版用户',
|
||||
},
|
||||
];
|
||||
|
||||
export default function DocsSeoSection() {
|
||||
const { language } = useLanguage();
|
||||
const zh = language === 'zh';
|
||||
|
||||
return (
|
||||
<section className="docs-seo" id="docs">
|
||||
<div className="container">
|
||||
<div className="section-header reveal">
|
||||
<div className="eyebrow">Guides</div>
|
||||
<h2 className="section-title">Image to LaTeX Guides</h2>
|
||||
<p className="section-desc">
|
||||
{zh
|
||||
? '为学生、研究者和数学写作者准备的一步步工作流指南。'
|
||||
: 'Step-by-step workflows for students, researchers, and anyone writing math.'}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="doc-cards reveal">
|
||||
{GUIDES.map((g, i) => (
|
||||
<Link key={i} to="/docs" className="doc-card">
|
||||
<div className="doc-card-left">
|
||||
<div className="doc-icon">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8">
|
||||
{g.svgPaths}
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<div className="doc-title">{zh ? g.titleZh : g.titleEn}</div>
|
||||
<div className="doc-meta">{zh ? g.metaZh : g.metaEn}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="doc-read">{zh ? '阅读指南 →' : 'Read Guide →'}</div>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -1,58 +1,60 @@
|
||||
import { PenTool, FileOutput, FileText, Layers, Zap, Gift } from 'lucide-react';
|
||||
import { useLanguage } from '../../contexts/LanguageContext';
|
||||
|
||||
const iconMap = [PenTool, FileOutput, FileText, Layers, Zap, Gift];
|
||||
const accentColors = [
|
||||
{ bg: 'bg-coral-50', icon: 'text-coral-500', border: 'hover:border-coral-200' },
|
||||
{ bg: 'bg-sage-50', icon: 'text-sage-600', border: 'hover:border-sage-200' },
|
||||
{ bg: 'bg-amber-50', icon: 'text-amber-600', border: 'hover:border-amber-200' },
|
||||
{ bg: 'bg-violet-50', icon: 'text-violet-500', border: 'hover:border-violet-200' },
|
||||
{ bg: 'bg-coral-50', icon: 'text-coral-500', border: 'hover:border-coral-200' },
|
||||
{ bg: 'bg-sage-50', icon: 'text-sage-600', border: 'hover:border-sage-200' },
|
||||
];
|
||||
|
||||
export default function FeaturesSection() {
|
||||
const { t } = useLanguage();
|
||||
const f = t.marketing.features;
|
||||
|
||||
const items = [
|
||||
{ title: f.handwriting, description: f.handwritingDesc },
|
||||
{ title: f.multiFormat, description: f.multiFormatDesc },
|
||||
{ title: f.pdf, description: f.pdfDesc },
|
||||
{ title: f.batch, description: f.batchDesc },
|
||||
{ title: f.accuracy, description: f.accuracyDesc },
|
||||
{ title: f.free, description: f.freeDesc },
|
||||
];
|
||||
const { language } = useLanguage();
|
||||
|
||||
return (
|
||||
<section id="features" className="section-padding bg-white relative">
|
||||
{/* Subtle top border accent */}
|
||||
<div className="absolute top-0 left-1/2 -translate-x-1/2 w-24 h-0.5 bg-gradient-to-r from-transparent via-coral-300 to-transparent" />
|
||||
|
||||
<div className="max-w-6xl mx-auto">
|
||||
<div className="max-w-2xl mb-16">
|
||||
<h2 className="font-display text-3xl lg:text-4xl font-bold text-ink mb-4 tracking-tight">{f.title}</h2>
|
||||
<p className="text-ink-secondary text-lg leading-relaxed">{f.subtitle}</p>
|
||||
<section className="core-features">
|
||||
<div className="container">
|
||||
<div className="section-header reveal">
|
||||
<div className="eyebrow">Core Features</div>
|
||||
<h2 className="section-title">
|
||||
{language === 'zh'
|
||||
? '学生留下来的三个理由'
|
||||
: 'Three reasons students stay with TexPixel'}
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-5">
|
||||
{items.map((item, i) => {
|
||||
const Icon = iconMap[i];
|
||||
const color = accentColors[i];
|
||||
return (
|
||||
<div
|
||||
key={i}
|
||||
className={`card p-6 ${color.border} group`}
|
||||
style={{ opacity: 0, animationDelay: `${i * 80}ms` }}
|
||||
>
|
||||
<div className={`w-11 h-11 ${color.bg} rounded-xl flex items-center justify-center mb-4 transition-transform duration-200 group-hover:scale-110`}>
|
||||
<Icon size={20} className={color.icon} />
|
||||
</div>
|
||||
<h3 className="font-display text-base font-semibold text-ink mb-2">{item.title}</h3>
|
||||
<p className="text-ink-secondary text-sm leading-relaxed">{item.description}</p>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
<div className="cards-3">
|
||||
<div className="feature-card reveal reveal-delay-1">
|
||||
<div className="feature-mini">
|
||||
<span className="feature-speed">⚡ t < 1s</span>
|
||||
</div>
|
||||
<div className="card-title">{language === 'zh' ? '极速识别' : 'Sub-second Recognition'}</div>
|
||||
<div className="card-desc">
|
||||
{language === 'zh'
|
||||
? '上传截图,LaTeX 随即出现。拍下笔记,无需等待,直接复制。'
|
||||
: 'Upload a screenshot, LaTeX appears instantly. Take a photo of your notes and copy right away.'}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="feature-card reveal reveal-delay-2">
|
||||
<div className="feature-mini" style={{ fontSize: '13px', color: 'var(--text-body)' }}>
|
||||
<span style={{ fontFamily: "'JetBrains Mono', monospace", color: 'var(--primary)' }}>
|
||||
\int_0^1 x^2 dx
|
||||
</span>
|
||||
</div>
|
||||
<div className="card-title">{language === 'zh' ? '复杂公式支持' : 'Complex Formula Support'}</div>
|
||||
<div className="card-desc">
|
||||
{language === 'zh'
|
||||
? '矩阵、积分、求和、化学式全部支持。多行公式对齐、角标嵌套一次识别。'
|
||||
: 'Matrices, integrals, summations, chemical formulas — all supported. Multi-line alignment and nested scripts in one pass.'}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="feature-card reveal reveal-delay-3">
|
||||
<div className="feature-mini">
|
||||
<span style={{ fontFamily: "'JetBrains Mono', monospace", color: 'var(--text-body)', fontSize: '13px' }}>
|
||||
\mathbf{'{A}'}<sup style={{ fontSize: '9px', color: 'var(--teal)' }}>−1</sup>b
|
||||
</span>
|
||||
</div>
|
||||
<div className="card-title">{language === 'zh' ? '高准确度' : 'High Accuracy'}</div>
|
||||
<div className="card-desc">
|
||||
{language === 'zh'
|
||||
? '论文级别识别准确率。在 arXiv 截图测试集上准确率超过 95%,持续迭代提升中。'
|
||||
: 'Publication-grade accuracy. Over 95% on arXiv screenshot benchmarks, continuously improving.'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -1,121 +1,153 @@
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { ArrowRight } from 'lucide-react';
|
||||
import { useLanguage } from '../../contexts/LanguageContext';
|
||||
|
||||
const LATEX_LINES = [
|
||||
'<span class="code-kw">\\frac</span>{-b \\pm <span class="code-kw">\\sqrt</span>{b² - 4ac}}{2a}',
|
||||
'<span class="code-kw">\\int</span>_0^1 x^2\\,dx',
|
||||
'<span class="code-kw">\\sum</span>_{i=<span class="code-num">1</span>}^n \\frac{1}{i^2}',
|
||||
];
|
||||
|
||||
export default function HeroSection() {
|
||||
const { t, language } = useLanguage();
|
||||
const { language } = useLanguage();
|
||||
const codeRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// Typing cycling effect
|
||||
useEffect(() => {
|
||||
let idx = 0;
|
||||
const interval = setInterval(() => {
|
||||
idx = (idx + 1) % LATEX_LINES.length;
|
||||
if (codeRef.current) {
|
||||
codeRef.current.innerHTML = LATEX_LINES[idx] + '<span class="cursor-blink"></span>';
|
||||
}
|
||||
}, 3500);
|
||||
return () => clearInterval(interval);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<section className="relative overflow-hidden min-h-[90vh] flex items-center">
|
||||
{/* Warm gradient background */}
|
||||
<div className="absolute inset-0 bg-gradient-to-br from-cream-100 via-cream-50 to-coral-50/30" />
|
||||
|
||||
{/* Decorative elements */}
|
||||
<div className="absolute top-20 right-[15%] w-72 h-72 bg-coral-200/20 rounded-full blur-3xl" />
|
||||
<div className="absolute bottom-20 left-[10%] w-56 h-56 bg-sage-200/20 rounded-full blur-3xl" />
|
||||
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[600px] h-[600px] bg-gradient-radial from-coral-100/10 to-transparent rounded-full" />
|
||||
|
||||
{/* Grid pattern */}
|
||||
<div
|
||||
className="absolute inset-0 opacity-[0.03]"
|
||||
style={{
|
||||
backgroundImage: `linear-gradient(var(--color-ink) 1px, transparent 1px), linear-gradient(90deg, var(--color-ink) 1px, transparent 1px)`,
|
||||
backgroundSize: '60px 60px',
|
||||
}}
|
||||
/>
|
||||
|
||||
<div className="relative max-w-6xl mx-auto px-6 py-20 lg:py-28">
|
||||
<div className="grid lg:grid-cols-12 gap-12 lg:gap-8 items-center">
|
||||
{/* Left: Text — takes 7 cols for asymmetry */}
|
||||
<div className="lg:col-span-7 animate-fade-up">
|
||||
{/* Badge */}
|
||||
<div className="inline-flex items-center gap-2 px-3 py-1.5 bg-white/80 backdrop-blur-sm border border-cream-300 rounded-full text-xs font-medium text-ink-secondary mb-8">
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-sage-500 animate-pulse" />
|
||||
{language === 'zh' ? '免费使用 · 无需注册' : 'Free to use · No sign-up required'}
|
||||
</div>
|
||||
|
||||
<h1 className="font-display text-4xl sm:text-5xl lg:text-[3.5rem] font-extrabold text-ink leading-[1.1] mb-6 tracking-tight">
|
||||
{t.marketing.hero.title}
|
||||
<section className="hero">
|
||||
<div className="container">
|
||||
<div className="hero-inner">
|
||||
<div className="hero-left">
|
||||
<h1 className="hero-title">
|
||||
{language === 'zh' ? (
|
||||
<>数学公式<br />秒级转换为<br /><em className="latex-word">LaTeX</em></>
|
||||
) : (
|
||||
<>Math Formulas<br />Converted to<br /><em className="latex-word">LaTeX</em></>
|
||||
)}
|
||||
</h1>
|
||||
|
||||
<p className="text-lg text-ink-secondary leading-relaxed mb-10 max-w-xl">
|
||||
{t.marketing.hero.subtitle}
|
||||
<p className="hero-desc">
|
||||
{language === 'zh'
|
||||
? 'AI 驱动的复杂数学公式识别,支持手写与论文截图,毫秒级输出 LaTeX、Markdown 与 Word 原生公式。'
|
||||
: 'AI-powered recognition for complex math formulas. Supports handwriting and paper screenshots. Instant LaTeX, Markdown, and Word output.'}
|
||||
</p>
|
||||
|
||||
<div className="flex flex-wrap gap-4">
|
||||
<Link
|
||||
to="/app"
|
||||
className="btn-primary px-7 py-3.5 text-base"
|
||||
>
|
||||
{t.marketing.hero.cta}
|
||||
<ArrowRight size={18} />
|
||||
<div className="hero-actions">
|
||||
<Link to="/app" className="btn btn-primary">
|
||||
<svg width="17" height="17" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.2">
|
||||
<path d="M5 3l14 9-14 9V3z" />
|
||||
</svg>
|
||||
{language === 'zh' ? '免费试用 TexPixel' : 'Try TexPixel Free'}
|
||||
</Link>
|
||||
<a
|
||||
href="#features"
|
||||
className="btn-secondary px-7 py-3.5 text-base"
|
||||
>
|
||||
{t.marketing.hero.ctaSecondary}
|
||||
<a href="#products" className="btn btn-secondary">
|
||||
{language === 'zh' ? '了解更多 →' : 'Learn More →'}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{/* Social proof hint */}
|
||||
<div className="mt-10 flex items-center gap-3 text-sm text-ink-muted">
|
||||
<div className="flex -space-x-2">
|
||||
{['bg-coral-300', 'bg-sage-300', 'bg-amber-300', 'bg-violet-300'].map((bg, i) => (
|
||||
<div key={i} className={`w-7 h-7 rounded-full ${bg} border-2 border-cream-50`} />
|
||||
))}
|
||||
<div className="hero-trust">
|
||||
<div className="trust-item">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.2">
|
||||
<circle cx="12" cy="12" r="10" /><path d="M12 8v4l3 3" />
|
||||
</svg>
|
||||
{language === 'zh' ? '秒级输出' : 'Sub-second output'}
|
||||
</div>
|
||||
<div className="trust-item">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.2">
|
||||
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" />
|
||||
<polyline points="14 2 14 8 20 8" />
|
||||
</svg>
|
||||
{language === 'zh' ? '支持 PDF · 手写 · 截图' : 'PDF · Handwriting · Screenshots'}
|
||||
</div>
|
||||
<div className="trust-item">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.2">
|
||||
<path d="M20 7H4a2 2 0 0 0-2 2v6a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V9a2 2 0 0 0-2-2z" />
|
||||
<circle cx="12" cy="12" r="1" />
|
||||
</svg>
|
||||
{language === 'zh' ? '免费套餐可用' : 'Free plan available'}
|
||||
</div>
|
||||
<span>{language === 'zh' ? '已有 1,000+ 学生在使用' : 'Used by 1,000+ students'}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right: Interactive demo card — 5 cols */}
|
||||
<div className="lg:col-span-5 animate-fade-up delay-200" style={{ opacity: 0 }}>
|
||||
<Link
|
||||
to="/app"
|
||||
className="block group"
|
||||
>
|
||||
<div className="card p-1 hover:border-coral-200">
|
||||
{/* Mock window chrome */}
|
||||
<div className="flex items-center gap-1.5 px-4 py-3 border-b border-cream-300/50">
|
||||
<div className="w-2.5 h-2.5 rounded-full bg-coral-300" />
|
||||
<div className="w-2.5 h-2.5 rounded-full bg-amber-300" />
|
||||
<div className="w-2.5 h-2.5 rounded-full bg-sage-300" />
|
||||
<span className="ml-3 text-[10px] font-mono text-ink-muted">texpixel.com/app</span>
|
||||
<div className="hero-right">
|
||||
<div className="mock-window">
|
||||
<div className="window-topbar">
|
||||
<div className="window-dots">
|
||||
<div className="window-dot wd-red" />
|
||||
<div className="window-dot wd-yellow" />
|
||||
<div className="window-dot wd-green" />
|
||||
</div>
|
||||
<div className="window-url">
|
||||
<svg className="url-lock" viewBox="0 0 12 14">
|
||||
<rect x="1" y="6" width="10" height="7" rx="2" />
|
||||
<path d="M3 6V4a3 3 0 0 1 6 0v2" fill="none" stroke="#8CC9BE" strokeWidth="1.4" />
|
||||
</svg>
|
||||
texpixel.com/app
|
||||
</div>
|
||||
</div>
|
||||
<div className="window-body">
|
||||
<div className="upload-zone">
|
||||
<div className="upload-icon-wrap">
|
||||
<svg viewBox="0 0 24 24">
|
||||
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
|
||||
<polyline points="17 8 12 3 7 8" />
|
||||
<line x1="12" y1="3" x2="12" y2="15" />
|
||||
</svg>
|
||||
</div>
|
||||
<div className="upload-text">
|
||||
{language === 'zh' ? '点击、拖拽或粘贴文件开始解析' : 'Click, drag or paste a file to start'}
|
||||
</div>
|
||||
<div className="upload-sub">
|
||||
{language === 'zh' ? '支持 PNG · JPG · PDF · 手写截图' : 'PNG · JPG · PDF · Handwriting'}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-6">
|
||||
{/* Upload zone */}
|
||||
<div className="border-2 border-dashed border-cream-300 group-hover:border-coral-300 rounded-xl p-8 flex flex-col items-center justify-center transition-colors duration-300 bg-cream-50/50">
|
||||
<div className="w-14 h-14 bg-coral-50 group-hover:bg-coral-100 rounded-2xl flex items-center justify-center mb-3 transition-all duration-300 group-hover:scale-110">
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="text-coral-500">
|
||||
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
|
||||
<polyline points="17 8 12 3 7 8" />
|
||||
<line x1="12" y1="3" x2="12" y2="15" />
|
||||
</svg>
|
||||
</div>
|
||||
<p className="text-ink-muted text-xs text-center">
|
||||
{t.sidebar.uploadInstruction}
|
||||
</p>
|
||||
<div className="output-zone">
|
||||
<div className="output-header">
|
||||
<span className="output-label">LaTeX Output</span>
|
||||
<span className="output-badge">{language === 'zh' ? '识别完成' : 'Done'}</span>
|
||||
</div>
|
||||
<div
|
||||
className="output-code"
|
||||
ref={codeRef}
|
||||
// eslint-disable-next-line react/no-danger
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: LATEX_LINES[0] + '<span class="cursor-blink"></span>',
|
||||
}}
|
||||
/>
|
||||
<div className="output-actions">
|
||||
<button className="output-btn output-btn-copy">
|
||||
{language === 'zh' ? '复制 LaTeX' : 'Copy LaTeX'}
|
||||
</button>
|
||||
<button className="output-btn output-btn-word">
|
||||
{language === 'zh' ? '复制到 Word' : 'Copy to Word'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Mock result */}
|
||||
<div className="mt-4 rounded-lg p-4 bg-ink/[0.03]">
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<div className="w-1.5 h-1.5 rounded-full bg-sage-500" />
|
||||
<span className="text-[10px] text-ink-muted font-mono tracking-wider uppercase">LaTeX Output</span>
|
||||
</div>
|
||||
<code className="text-sm text-ink font-mono block leading-relaxed">
|
||||
{'\\frac{-b \\pm \\sqrt{b^2 - 4ac}}{2a}'}
|
||||
</code>
|
||||
<div className="window-status">
|
||||
<div className="status-time">{language === 'zh' ? '识别耗时 0.38s' : 'Recognized in 0.38s'}</div>
|
||||
<div className="status-format">
|
||||
<div className="fmt-tag">LaTeX</div>
|
||||
<div className="fmt-tag">Markdown</div>
|
||||
<div className="fmt-tag">Word</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,56 +0,0 @@
|
||||
import { Upload, Cpu, Download } from 'lucide-react';
|
||||
import { useLanguage } from '../../contexts/LanguageContext';
|
||||
|
||||
const icons = [Upload, Cpu, Download];
|
||||
const stepColors = [
|
||||
{ num: 'text-coral-500', ring: 'ring-coral-100', bg: 'bg-coral-50' },
|
||||
{ num: 'text-sage-600', ring: 'ring-sage-100', bg: 'bg-sage-50' },
|
||||
{ num: 'text-amber-600', ring: 'ring-amber-100', bg: 'bg-amber-50' },
|
||||
];
|
||||
|
||||
export default function HowItWorksSection() {
|
||||
const { t } = useLanguage();
|
||||
const h = t.marketing.howItWorks;
|
||||
|
||||
const steps = [
|
||||
{ title: h.step1, description: h.step1Desc },
|
||||
{ title: h.step2, description: h.step2Desc },
|
||||
{ title: h.step3, description: h.step3Desc },
|
||||
];
|
||||
|
||||
return (
|
||||
<section className="section-padding relative" style={{ backgroundColor: 'var(--color-bg)' }}>
|
||||
<div className="max-w-6xl mx-auto">
|
||||
<div className="text-center mb-16">
|
||||
<h2 className="font-display text-3xl lg:text-4xl font-bold text-ink mb-4 tracking-tight">{h.title}</h2>
|
||||
</div>
|
||||
|
||||
<div className="grid md:grid-cols-3 gap-8 lg:gap-12 relative">
|
||||
{/* Connecting line (desktop only) */}
|
||||
<div className="hidden md:block absolute top-12 left-[20%] right-[20%] h-px border-t-2 border-dashed border-cream-300" />
|
||||
|
||||
{steps.map((step, i) => {
|
||||
const Icon = icons[i];
|
||||
const color = stepColors[i];
|
||||
return (
|
||||
<div key={i} className="relative text-center group">
|
||||
{/* Step number badge */}
|
||||
<div className="relative inline-flex flex-col items-center mb-6">
|
||||
<div className={`w-24 h-24 ${color.bg} rounded-3xl flex items-center justify-center ring-4 ${color.ring} ring-offset-4 ring-offset-[var(--color-bg)] transition-transform duration-300 group-hover:scale-105 relative z-10`}>
|
||||
<Icon size={32} className={color.num} strokeWidth={1.5} />
|
||||
</div>
|
||||
<span className={`absolute -top-3 -right-3 w-8 h-8 rounded-full bg-white shadow-md flex items-center justify-center font-display font-bold text-sm ${color.num} z-20`}>
|
||||
{String(i + 1).padStart(2, '0')}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<h3 className="font-display text-xl font-semibold text-ink mb-3">{step.title}</h3>
|
||||
<p className="text-ink-secondary text-sm leading-relaxed max-w-xs mx-auto">{step.description}</p>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -1,114 +1,81 @@
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Check } from 'lucide-react';
|
||||
import { useLanguage } from '../../contexts/LanguageContext';
|
||||
|
||||
export default function PricingSection() {
|
||||
const { t } = useLanguage();
|
||||
const p = t.marketing.pricing;
|
||||
|
||||
const plans = [
|
||||
{
|
||||
name: p.free,
|
||||
price: '$0',
|
||||
period: p.monthly,
|
||||
features: p.freeFeatures,
|
||||
cta: p.getStarted,
|
||||
href: '/app',
|
||||
disabled: false,
|
||||
popular: false,
|
||||
accent: 'sage',
|
||||
},
|
||||
{
|
||||
name: p.pro,
|
||||
price: '$9.9',
|
||||
period: p.monthly,
|
||||
features: p.proFeatures,
|
||||
cta: p.comingSoon,
|
||||
href: '#',
|
||||
disabled: true,
|
||||
popular: true,
|
||||
accent: 'coral',
|
||||
},
|
||||
{
|
||||
name: p.enterprise,
|
||||
price: p.custom,
|
||||
period: '',
|
||||
features: p.enterpriseFeatures,
|
||||
cta: p.contactUs,
|
||||
href: '#contact',
|
||||
disabled: false,
|
||||
popular: false,
|
||||
accent: 'ink',
|
||||
},
|
||||
];
|
||||
const { language } = useLanguage();
|
||||
const zh = language === 'zh';
|
||||
|
||||
return (
|
||||
<section id="pricing" className="section-padding bg-white">
|
||||
<div className="max-w-6xl mx-auto">
|
||||
<div className="text-center mb-16">
|
||||
<h2 className="font-display text-3xl lg:text-4xl font-bold text-ink mb-4 tracking-tight">{p.title}</h2>
|
||||
<p className="text-ink-secondary text-lg">{p.subtitle}</p>
|
||||
<section className="pricing" id="pricing">
|
||||
<div className="container">
|
||||
<div className="section-header reveal">
|
||||
<div className="eyebrow">Pricing</div>
|
||||
<h2 className="section-title">
|
||||
{zh ? '选择适合你的方案' : 'Choose the plan that fits your workflow'}
|
||||
</h2>
|
||||
<p className="section-desc">
|
||||
{zh ? '从免费试用到永久桌面版,按需选择,无需绑定订阅。' : 'From free trial to lifetime desktop — pick what fits, no subscription lock-in.'}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid md:grid-cols-3 gap-6 max-w-5xl mx-auto">
|
||||
{plans.map((plan, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className={`relative rounded-2xl p-7 flex flex-col transition-all duration-200 ${
|
||||
plan.popular
|
||||
? 'border-2 border-coral-400 shadow-xl shadow-coral-500/10 scale-[1.02]'
|
||||
: 'card'
|
||||
}`}
|
||||
>
|
||||
{plan.popular && (
|
||||
<div className="absolute -top-3.5 left-1/2 -translate-x-1/2 bg-gradient-to-r from-coral-500 to-coral-400 text-white text-xs font-display font-semibold px-4 py-1 rounded-full shadow-md">
|
||||
{p.popular}
|
||||
</div>
|
||||
)}
|
||||
<div className="pricing-cards">
|
||||
<div className="pricing-card reveal reveal-delay-1">
|
||||
<div className="plan-name">Free</div>
|
||||
<div className="plan-price">$<span style={{ fontSize: '44px', color: 'var(--text-strong)' }}>0</span></div>
|
||||
<div className="plan-period">{zh ? '永久免费' : 'Forever free'}</div>
|
||||
<div className="plan-desc">For first-time use and quick screenshots.</div>
|
||||
<ul className="plan-features">
|
||||
<li>{zh ? '每月 30 次识别' : '30 recognitions / month'}</li>
|
||||
<li>LaTeX {zh ? '输出' : 'output'}</li>
|
||||
<li>Web App {zh ? '访问' : 'access'}</li>
|
||||
<li>{zh ? '基础公式支持' : 'Basic formula support'}</li>
|
||||
</ul>
|
||||
<Link to="/app" className="plan-btn">{zh ? '开始使用' : 'Get Started'}</Link>
|
||||
</div>
|
||||
|
||||
<h3 className="font-display text-lg font-bold text-ink mb-1">{plan.name}</h3>
|
||||
<div className="mb-6">
|
||||
<span className="font-display text-4xl font-extrabold text-ink">{plan.price}</span>
|
||||
{plan.period && <span className="text-ink-muted text-sm ml-1">{plan.period}</span>}
|
||||
</div>
|
||||
<div className="pricing-card reveal reveal-delay-2">
|
||||
<div className="plan-name">Monthly</div>
|
||||
<div className="plan-price"><span>$</span>9</div>
|
||||
<div className="plan-period">{zh ? '每月 / 随时取消' : '/month · cancel anytime'}</div>
|
||||
<div className="plan-desc">Unlimited recognition for everyday study.</div>
|
||||
<ul className="plan-features">
|
||||
<li>{zh ? '无限次识别' : 'Unlimited recognitions'}</li>
|
||||
<li>LaTeX + Markdown</li>
|
||||
<li>{zh ? 'Word 原生公式' : 'Native Word equations'}</li>
|
||||
<li>{zh ? '优先处理队列' : 'Priority queue'}</li>
|
||||
</ul>
|
||||
<Link to="/app" className="plan-btn">{zh ? '开始使用' : 'Get Started'}</Link>
|
||||
</div>
|
||||
|
||||
<ul className="space-y-3 mb-8 flex-1">
|
||||
{plan.features.map((feature, j) => (
|
||||
<li key={j} className="flex items-start gap-2.5 text-sm text-ink-secondary">
|
||||
<Check size={15} className="text-sage-500 mt-0.5 flex-shrink-0" />
|
||||
{feature}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<div className="pricing-card reveal reveal-delay-3">
|
||||
<div className="plan-name">Quarterly</div>
|
||||
<div className="plan-price"><span>$</span>24</div>
|
||||
<div className="plan-period">{zh ? '每季 · 省 $3/月' : '/quarter · save $3/mo'}</div>
|
||||
<div className="plan-desc">Best value for semester-long heavy usage.</div>
|
||||
<ul className="plan-features">
|
||||
<li>{zh ? '无限次识别' : 'Unlimited recognitions'}</li>
|
||||
<li>{zh ? '全格式输出' : 'All output formats'}</li>
|
||||
<li>{zh ? '批量 PDF 提取' : 'Batch PDF extraction'}</li>
|
||||
<li>API {zh ? '访问(Beta)' : 'access (Beta)'}</li>
|
||||
</ul>
|
||||
<Link to="/app" className="plan-btn">{zh ? '开始使用' : 'Get Started'}</Link>
|
||||
</div>
|
||||
|
||||
{plan.disabled ? (
|
||||
<button
|
||||
disabled
|
||||
className="w-full py-3 rounded-xl text-sm font-semibold bg-cream-200 text-ink-muted cursor-not-allowed"
|
||||
>
|
||||
{plan.cta}
|
||||
</button>
|
||||
) : plan.href.startsWith('/') ? (
|
||||
<Link
|
||||
to={plan.href}
|
||||
className={`w-full py-3 rounded-xl text-sm font-semibold text-center block transition-all duration-200 ${
|
||||
plan.popular
|
||||
? 'btn-primary justify-center'
|
||||
: 'bg-ink hover:bg-ink/90 text-white shadow-sm'
|
||||
}`}
|
||||
>
|
||||
{plan.cta}
|
||||
</Link>
|
||||
) : (
|
||||
<a
|
||||
href={plan.href}
|
||||
className="w-full py-3 rounded-xl text-sm font-semibold text-center block bg-ink hover:bg-ink/90 text-white transition-colors shadow-sm"
|
||||
>
|
||||
{plan.cta}
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
<div className="pricing-card featured reveal">
|
||||
<div className="featured-badge">{zh ? '永久版' : 'Lifetime'}</div>
|
||||
<div className="plan-name">Desktop</div>
|
||||
<div className="plan-price"><span>$</span>79</div>
|
||||
<div className="plan-period">{zh ? '一次购买 · 终身使用' : 'one-time · lifetime access'}</div>
|
||||
<div className="plan-desc">Lifetime access for offline and sensitive files.</div>
|
||||
<ul className="plan-features">
|
||||
<li>{zh ? '完全离线运行' : 'Fully offline'}</li>
|
||||
<li>{zh ? '无限次识别' : 'Unlimited recognitions'}</li>
|
||||
<li>{zh ? '批量 PDF 处理' : 'Batch PDF processing'}</li>
|
||||
<li>{zh ? '本地隐私保护' : 'Local privacy'}</li>
|
||||
<li>{zh ? '终身免费更新' : 'Lifetime free updates'}</li>
|
||||
</ul>
|
||||
<Link to="/app" className="plan-btn featured-btn">{zh ? '购买桌面版' : 'Buy Desktop'}</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
81
src/components/home/ProductSuiteSection.tsx
Normal file
81
src/components/home/ProductSuiteSection.tsx
Normal file
@@ -0,0 +1,81 @@
|
||||
import { Link } from 'react-router-dom';
|
||||
import { useLanguage } from '../../contexts/LanguageContext';
|
||||
|
||||
export default function ProductSuiteSection() {
|
||||
const { language } = useLanguage();
|
||||
|
||||
return (
|
||||
<section className="product-suite" id="products">
|
||||
<div className="container">
|
||||
<div className="section-header reveal">
|
||||
<div className="eyebrow">Product Matrix</div>
|
||||
<h2 className="section-title">
|
||||
{language === 'zh' ? '覆盖所有公式工作流' : 'Built for every formula workflow'}
|
||||
</h2>
|
||||
<p className="section-desc">
|
||||
{language === 'zh'
|
||||
? '从浏览器即取即用,到扩展一键复制,再到桌面端离线处理——一个工具,覆盖学生写作业、研究者整理文献、工程师记录推导的全部场景。'
|
||||
: 'From instant browser use to one-click extension copy to offline desktop — one tool for students, researchers, and engineers.'}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="cards-3">
|
||||
<div className="product-card reveal reveal-delay-1">
|
||||
<div className="card-icon icon-orange">
|
||||
<svg 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>
|
||||
</div>
|
||||
<div className="card-title">Web App</div>
|
||||
<div className="card-desc">
|
||||
{language === 'zh'
|
||||
? '浏览器内即时识别,无需安装。上传截图或手写图片,秒级输出 LaTeX。'
|
||||
: 'Instant recognition in browser, no install needed. Upload screenshot or handwriting, get LaTeX in seconds.'}
|
||||
</div>
|
||||
<Link to="/app" className="card-link">
|
||||
{language === 'zh' ? '浏览器即时识别 →' : 'Instant formula recognition in browser →'}
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div className="product-card reveal reveal-delay-2">
|
||||
<div className="card-icon icon-teal">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8">
|
||||
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" />
|
||||
<polyline points="14 2 14 8 20 8" />
|
||||
<path d="M8 13h8M8 17h5" />
|
||||
</svg>
|
||||
</div>
|
||||
<div className="card-title">Extension</div>
|
||||
<div className="card-desc">
|
||||
{language === 'zh'
|
||||
? '浏览器扩展,一键将 ChatGPT、Claude 输出的公式复制为 Word 原生数学公式。'
|
||||
: 'Browser extension — one-click copy of formulas from ChatGPT or Claude as native Word equations.'}
|
||||
</div>
|
||||
<a href="#" className="card-link">
|
||||
{language === 'zh' ? '从 LLM 复制公式到 Word →' : 'Copy formulas from LLMs to Word →'}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div className="product-card reveal reveal-delay-3">
|
||||
<div className="card-icon icon-lavender">
|
||||
<svg 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="M2 8h20M8 3v5" />
|
||||
</svg>
|
||||
</div>
|
||||
<div className="card-title">Desktop</div>
|
||||
<div className="card-desc">
|
||||
{language === 'zh'
|
||||
? '桌面端离线处理,适合论文批量提取与隐私保护场景,一次购买终身使用。'
|
||||
: 'Offline desktop app for batch PDF extraction and privacy-sensitive work. One-time purchase.'}
|
||||
</div>
|
||||
<a href="#pricing" className="card-link">
|
||||
{language === 'zh' ? '查看桌面版定价 →' : 'See Desktop pricing →'}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
86
src/components/home/ShowcaseSection.tsx
Normal file
86
src/components/home/ShowcaseSection.tsx
Normal file
@@ -0,0 +1,86 @@
|
||||
import { Link } from 'react-router-dom';
|
||||
import { useLanguage } from '../../contexts/LanguageContext';
|
||||
|
||||
export default function ShowcaseSection() {
|
||||
const { language } = useLanguage();
|
||||
|
||||
return (
|
||||
<section className="showcase">
|
||||
<div className="container">
|
||||
<div className="section-header reveal">
|
||||
<div className="eyebrow">Live Examples</div>
|
||||
<h2 className="section-title">
|
||||
{language === 'zh' ? '真实案例演示' : 'Try Real Examples'}
|
||||
</h2>
|
||||
<p className="section-desc">
|
||||
{language === 'zh'
|
||||
? '看看 TexPixel 如何处理真实的论文截图与手写笔记。'
|
||||
: 'See how TexPixel handles real paper screenshots and handwritten notes.'}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="showcase-cards">
|
||||
<div className="showcase-card reveal reveal-delay-1">
|
||||
<div className="showcase-header">
|
||||
<div className="showcase-tag">{language === 'zh' ? '复杂公式' : 'Complex Formula'}</div>
|
||||
<div className="showcase-title">
|
||||
{language === 'zh' ? '论文截图到可复制 LaTeX' : 'Paper Screenshot to Copyable LaTeX'}
|
||||
</div>
|
||||
<div className="showcase-sub">arXiv PDF · 0.41s</div>
|
||||
</div>
|
||||
<div className="showcase-body">
|
||||
<div className="sc-input">
|
||||
<span style={{ fontFamily: "'JetBrains Mono', monospace", fontSize: '15px', opacity: 0.6 }}>
|
||||
∑ ¹/ᵢ² · · · [{language === 'zh' ? '图片占位' : 'image placeholder'}]
|
||||
</span>
|
||||
</div>
|
||||
<div className="sc-arrow">↓</div>
|
||||
<div
|
||||
className="sc-output"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: '<span class="kw">\\sum</span>_{i=<span class="num">1</span>}^{n}<span class="kw">\\frac</span>{<span class="num">1</span>}{i^<span class="num">2</span>}',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="showcase-footer">
|
||||
<Link to="/app" className="btn btn-secondary" style={{ height: '44px', fontSize: '14px', padding: '0 20px' }}>
|
||||
{language === 'zh' ? '试试这个例子 →' : 'Try this example →'}
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="showcase-card reveal reveal-delay-2">
|
||||
<div className="showcase-header">
|
||||
<div className="showcase-tag" style={{ background: 'rgba(140,201,190,0.15)', color: 'var(--teal)' }}>
|
||||
{language === 'zh' ? '手写公式' : 'Handwritten Formula'}
|
||||
</div>
|
||||
<div className="showcase-title">
|
||||
{language === 'zh' ? '课堂笔记到标准表达式' : 'Classroom Notes to Standard Expression'}
|
||||
</div>
|
||||
<div className="showcase-sub">{language === 'zh' ? '手机拍摄笔记 · 0.38s' : 'Phone photo of notes · 0.38s'}</div>
|
||||
</div>
|
||||
<div className="showcase-body">
|
||||
<div className="sc-input">
|
||||
<span style={{ fontFamily: "'JetBrains Mono', monospace", fontSize: '15px', opacity: 0.6 }}>
|
||||
lim sin(x)/x [{language === 'zh' ? '手写' : 'handwritten'}]
|
||||
</span>
|
||||
</div>
|
||||
<div className="sc-arrow">↓</div>
|
||||
<div
|
||||
className="sc-output"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: '<span class="kw">\\lim</span>_{x<span class="br">\\to</span> <span class="num">0</span>}<span class="kw">\\frac</span>{<span class="kw">\\sin</span> x}{x}',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="showcase-footer">
|
||||
<Link to="/app" className="btn btn-secondary" style={{ height: '44px', fontSize: '14px', padding: '0 20px' }}>
|
||||
{language === 'zh' ? '试试这个例子 →' : 'Try this example →'}
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
183
src/components/home/TestimonialsSection.tsx
Normal file
183
src/components/home/TestimonialsSection.tsx
Normal file
@@ -0,0 +1,183 @@
|
||||
import { useState, useEffect, useCallback, useRef } from 'react';
|
||||
import { useLanguage } from '../../contexts/LanguageContext';
|
||||
|
||||
const TESTIMONIALS = [
|
||||
{
|
||||
quote: '写论文的时候截图粘进去,LaTeX 就出来了。以前每个公式都要手敲,现在一个截图解决,省了我大概 40% 的时间。',
|
||||
name: '林同学',
|
||||
role: '数学系研究生 · 北京大学',
|
||||
avatarBg: 'var(--teal)',
|
||||
avatarColor: '#1a5c54',
|
||||
avatarLetter: '林',
|
||||
stars: '★★★★★',
|
||||
},
|
||||
{
|
||||
quote: '手写推导拍一张,马上就是干净的 LaTeX。对我这种每天要整理大量笔记的人来说,真的是刚需级别的工具。',
|
||||
name: '田晓雯',
|
||||
role: '物理学博士候选人 · 清华大学',
|
||||
avatarBg: 'var(--lavender)',
|
||||
avatarColor: '#3d3870',
|
||||
avatarLetter: '田',
|
||||
stars: '★★★★★',
|
||||
},
|
||||
{
|
||||
quote: "I use it every day for my thesis. The accuracy on complex integrals and matrix expressions is surprisingly good — way better than anything I've tried before.",
|
||||
name: 'Sarah M.',
|
||||
role: 'Applied Math PhD · MIT',
|
||||
avatarBg: 'var(--rose)',
|
||||
avatarColor: '#7a2e1e',
|
||||
avatarLetter: 'S',
|
||||
stars: '★★★★★',
|
||||
},
|
||||
{
|
||||
quote: 'Desktop 版的离线功能对我来说很重要,论文数据不想上传到云端。买断价格也合理,不用每个月担心订阅。',
|
||||
name: '陈博士',
|
||||
role: '生物信息学研究员 · 中科院',
|
||||
avatarBg: 'var(--gold)',
|
||||
avatarColor: '#6f5800',
|
||||
avatarLetter: '陈',
|
||||
stars: '★★★★★',
|
||||
},
|
||||
{
|
||||
quote: 'The browser extension is a game changer. I copy equations from Claude or ChatGPT straight into Word as native math — no more reformatting nightmares.',
|
||||
name: 'Alex K.',
|
||||
role: 'Engineering Student · TU Berlin',
|
||||
avatarBg: '#8CB4C9',
|
||||
avatarColor: '#1a3d52',
|
||||
avatarLetter: 'A',
|
||||
stars: '★★★★★',
|
||||
},
|
||||
{
|
||||
quote: '教材扫描件里的公式以前完全没法用,现在截图一框就搞定。连化学方程式都能识别,超出我的预期。',
|
||||
name: '王梓涵',
|
||||
role: '化学工程本科 · 浙江大学',
|
||||
avatarBg: '#C9A88C',
|
||||
avatarColor: '#4a2e14',
|
||||
avatarLetter: '王',
|
||||
stars: '★★★★☆',
|
||||
},
|
||||
];
|
||||
|
||||
const VISIBLE = 3;
|
||||
const TOTAL = TESTIMONIALS.length;
|
||||
const PAGES = TOTAL - VISIBLE + 1; // 4
|
||||
|
||||
export default function TestimonialsSection() {
|
||||
const { language } = useLanguage();
|
||||
const [currentPage, setCurrentPage] = useState(0);
|
||||
const trackRef = useRef<HTMLDivElement>(null);
|
||||
const autoTimerRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
||||
|
||||
const goTo = useCallback((idx: number) => {
|
||||
const clamped = Math.max(0, Math.min(idx, PAGES - 1));
|
||||
setCurrentPage(clamped);
|
||||
if (trackRef.current) {
|
||||
const cardW = trackRef.current.parentElement!.offsetWidth;
|
||||
const gap = 20;
|
||||
const singleW = (cardW - gap * (VISIBLE - 1)) / VISIBLE;
|
||||
const offset = clamped * (singleW + gap);
|
||||
trackRef.current.style.transform = `translateX(-${offset}px)`;
|
||||
}
|
||||
}, []);
|
||||
|
||||
const resetAuto = useCallback(() => {
|
||||
if (autoTimerRef.current) clearInterval(autoTimerRef.current);
|
||||
autoTimerRef.current = setInterval(() => {
|
||||
setCurrentPage((prev) => {
|
||||
const next = (prev + 1) % PAGES;
|
||||
goTo(next);
|
||||
return next;
|
||||
});
|
||||
}, 5000);
|
||||
}, [goTo]);
|
||||
|
||||
useEffect(() => {
|
||||
resetAuto();
|
||||
return () => { if (autoTimerRef.current) clearInterval(autoTimerRef.current); };
|
||||
}, [resetAuto]);
|
||||
|
||||
useEffect(() => {
|
||||
const handleResize = () => goTo(currentPage);
|
||||
window.addEventListener('resize', handleResize);
|
||||
return () => window.removeEventListener('resize', handleResize);
|
||||
}, [currentPage, goTo]);
|
||||
|
||||
const handleGoTo = (idx: number) => {
|
||||
goTo(idx);
|
||||
resetAuto();
|
||||
};
|
||||
|
||||
return (
|
||||
<section className="user-love">
|
||||
<div className="container">
|
||||
<div className="section-header reveal">
|
||||
<div className="eyebrow">User Love</div>
|
||||
<h2 className="section-title">
|
||||
{language === 'zh' ? '全球学生都在用' : 'Loved by students worldwide'}
|
||||
</h2>
|
||||
<p className="section-desc">
|
||||
{language === 'zh' ? '来自真实用户的反馈。' : 'Feedback from real users.'}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="testimonial-wrap reveal">
|
||||
<div
|
||||
className="testimonial-track"
|
||||
ref={trackRef}
|
||||
style={{ transition: 'transform 0.4s ease' }}
|
||||
>
|
||||
{TESTIMONIALS.map((t, i) => (
|
||||
<div key={i} className="testimonial-card">
|
||||
<div className="t-quote">"</div>
|
||||
<p className="t-body">{t.quote}</p>
|
||||
<div className="t-footer">
|
||||
<div
|
||||
className="t-avatar"
|
||||
style={{ background: t.avatarBg, color: t.avatarColor }}
|
||||
>
|
||||
{t.avatarLetter}
|
||||
</div>
|
||||
<div>
|
||||
<div className="t-name">{t.name}</div>
|
||||
<div className="t-role">{t.role}</div>
|
||||
</div>
|
||||
<div className="t-stars">{t.stars}</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="testimonial-nav">
|
||||
<button
|
||||
className="t-nav-btn"
|
||||
onClick={() => handleGoTo(currentPage - 1)}
|
||||
aria-label={language === 'zh' ? '上一条' : 'Previous'}
|
||||
>
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
||||
<path d="M15 18l-6-6 6-6" />
|
||||
</svg>
|
||||
</button>
|
||||
<div className="t-dots">
|
||||
{Array.from({ length: PAGES }).map((_, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className={`t-dot${i === currentPage ? ' active' : ''}`}
|
||||
onClick={() => handleGoTo(i)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<button
|
||||
className="t-nav-btn"
|
||||
onClick={() => handleGoTo(currentPage + 1)}
|
||||
aria-label={language === 'zh' ? '下一条' : 'Next'}
|
||||
>
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
||||
<path d="M9 18l6-6-6-6" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -2,77 +2,75 @@ import { Link } from 'react-router-dom';
|
||||
import { useLanguage } from '../../contexts/LanguageContext';
|
||||
|
||||
export default function Footer() {
|
||||
const { t } = useLanguage();
|
||||
const { language } = useLanguage();
|
||||
const zh = language === 'zh';
|
||||
|
||||
return (
|
||||
<footer className="relative bg-ink text-cream-300 overflow-hidden">
|
||||
{/* Decorative top edge */}
|
||||
<div className="absolute top-0 left-0 right-0 h-px bg-gradient-to-r from-transparent via-coral-500/40 to-transparent" />
|
||||
|
||||
<div className="max-w-6xl mx-auto px-6 py-16">
|
||||
<div className="grid grid-cols-1 md:grid-cols-12 gap-10">
|
||||
{/* Brand column */}
|
||||
<div className="md:col-span-4">
|
||||
<div className="flex items-center gap-2.5 mb-4">
|
||||
<img src="/texpixel-app-icon.svg" alt="TexPixel" className="w-7 h-7" />
|
||||
<span className="text-white font-display font-bold text-lg">TexPixel</span>
|
||||
</div>
|
||||
<p className="text-sm text-cream-300/70 leading-relaxed max-w-xs">
|
||||
{t.marketing.footer.tagline}
|
||||
<footer>
|
||||
<div className="container">
|
||||
<div className="footer-grid">
|
||||
<div className="footer-brand">
|
||||
<Link to="/" className="footer-logo">
|
||||
<div className="logo-icon" style={{ width: '32px', height: '32px', borderRadius: '9px' }}>
|
||||
<svg viewBox="0 0 24 24" style={{ width: '18px', height: '18px' }} fill="none" stroke="white" strokeWidth="2">
|
||||
<path d="M4 6h16M4 10h10M4 14h12M4 18h8" />
|
||||
</svg>
|
||||
</div>
|
||||
<span>TexPixel</span>
|
||||
</Link>
|
||||
<p className="footer-tagline">
|
||||
{zh ? '为学生、研究者和数学写作者而设计。' : 'Designed for students, researchers, and anyone writing math.'}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Product */}
|
||||
<div className="md:col-span-2 md:col-start-6">
|
||||
<h4 className="text-white font-display font-semibold text-sm mb-4 tracking-wide uppercase">
|
||||
{t.marketing.footer.product}
|
||||
</h4>
|
||||
<div className="flex flex-col gap-2.5 text-sm">
|
||||
<Link to="/app" className="text-cream-300/70 hover:text-white transition-colors w-fit">
|
||||
{t.marketing.nav.launchApp}
|
||||
</Link>
|
||||
<a href="/#pricing" className="text-cream-300/70 hover:text-white transition-colors w-fit">
|
||||
{t.marketing.nav.pricing}
|
||||
</a>
|
||||
</div>
|
||||
<div>
|
||||
<div className="footer-col-title">Product</div>
|
||||
<ul className="footer-links">
|
||||
<li><Link to="/app">Web App</Link></li>
|
||||
<li><a href="#">Extension</a></li>
|
||||
<li><a href="#pricing">{zh ? '桌面版' : 'Desktop'}</a></li>
|
||||
<li><a href="#">API (Beta)</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{/* Resources */}
|
||||
<div className="md:col-span-2">
|
||||
<h4 className="text-white font-display font-semibold text-sm mb-4 tracking-wide uppercase">
|
||||
{t.marketing.footer.resources}
|
||||
</h4>
|
||||
<div className="flex flex-col gap-2.5 text-sm">
|
||||
<Link to="/docs" className="text-cream-300/70 hover:text-white transition-colors w-fit">
|
||||
{t.marketing.nav.docs}
|
||||
</Link>
|
||||
<Link to="/blog" className="text-cream-300/70 hover:text-white transition-colors w-fit">
|
||||
{t.marketing.nav.blog}
|
||||
</Link>
|
||||
</div>
|
||||
<div>
|
||||
<div className="footer-col-title">Docs</div>
|
||||
<ul className="footer-links">
|
||||
<li><Link to="/docs">Image to LaTeX</Link></li>
|
||||
<li><Link to="/docs">PDF to Markdown</Link></li>
|
||||
<li><Link to="/docs">{zh ? '手写识别' : 'Handwritten OCR'}</Link></li>
|
||||
<li><Link to="/docs">Word Equations</Link></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{/* Contact */}
|
||||
<div className="md:col-span-2">
|
||||
<h4 className="text-white font-display font-semibold text-sm mb-4 tracking-wide uppercase">
|
||||
{t.marketing.footer.contactTitle}
|
||||
</h4>
|
||||
<div className="flex flex-col gap-2.5 text-sm">
|
||||
<a href="mailto:yogecoder@gmail.com" className="text-cream-300/70 hover:text-white transition-colors w-fit">
|
||||
yogecoder@gmail.com
|
||||
</a>
|
||||
</div>
|
||||
<div>
|
||||
<div className="footer-col-title">{zh ? '公司' : 'Company'}</div>
|
||||
<ul className="footer-links">
|
||||
<li><a href="#">{zh ? '关于我们' : 'About'}</a></li>
|
||||
<li><a href="#pricing">{zh ? '价格' : 'Pricing'}</a></li>
|
||||
<li><Link to="/blog">{zh ? '博客' : 'Blog'}</Link></li>
|
||||
<li><a href="#">{zh ? '联系我们' : 'Contact'}</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="footer-col-title">{zh ? '法律' : 'Legal'}</div>
|
||||
<ul className="footer-links">
|
||||
<li><a href="#">{zh ? '服务条款' : 'Terms of Service'}</a></li>
|
||||
<li><a href="#">{zh ? '隐私政策' : 'Privacy Policy'}</a></li>
|
||||
<li><a href="#">{zh ? 'Cookie 政策' : 'Cookie Policy'}</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-12 pt-8 border-t border-white/10 flex flex-col sm:flex-row items-center justify-between gap-4">
|
||||
<p className="text-xs text-cream-300/50">
|
||||
© {new Date().getFullYear()} TexPixel. All rights reserved.
|
||||
</p>
|
||||
<div className="flex items-center gap-1 text-xs text-cream-300/40">
|
||||
<span>Built with</span>
|
||||
<span className="text-coral-400">care</span>
|
||||
<span>for students & researchers</span>
|
||||
<div className="footer-bottom">
|
||||
<div className="footer-copy">© 2026 TexPixel. All rights reserved.</div>
|
||||
<div className="footer-made">
|
||||
Made with{' '}
|
||||
<svg viewBox="0 0 24 24" style={{ display: 'inline', width: '14px', height: '14px', verticalAlign: 'middle', fill: 'var(--rose)' }}>
|
||||
<path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z" />
|
||||
</svg>
|
||||
{' '}{zh ? '为全球学生而作' : 'for students worldwide'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
import { Outlet } from 'react-router-dom';
|
||||
import MarketingNavbar from './MarketingNavbar';
|
||||
import Footer from './Footer';
|
||||
import '../../styles/landing.css';
|
||||
|
||||
export default function MarketingLayout() {
|
||||
return (
|
||||
<div className="min-h-screen flex flex-col bg-white">
|
||||
<div className="marketing-page">
|
||||
<div className="glow-blob glow-blob-1" />
|
||||
<div className="glow-blob glow-blob-2" />
|
||||
<div className="glow-blob glow-blob-3" />
|
||||
<MarketingNavbar />
|
||||
<main className="flex-1">
|
||||
<main>
|
||||
<Outlet />
|
||||
</main>
|
||||
<Footer />
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
21
src/hooks/useScrollReveal.ts
Normal file
21
src/hooks/useScrollReveal.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { useEffect } from 'react';
|
||||
|
||||
export function useScrollReveal() {
|
||||
useEffect(() => {
|
||||
const observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
entries.forEach((e) => {
|
||||
if (e.isIntersecting) {
|
||||
e.target.classList.add('visible');
|
||||
observer.unobserve(e.target);
|
||||
}
|
||||
});
|
||||
},
|
||||
{ threshold: 0.12, rootMargin: '0px 0px -40px 0px' }
|
||||
);
|
||||
|
||||
document.querySelectorAll('.reveal').forEach((el) => observer.observe(el));
|
||||
|
||||
return () => observer.disconnect();
|
||||
}, []);
|
||||
}
|
||||
@@ -112,7 +112,6 @@ export const translations = {
|
||||
docs: 'Docs',
|
||||
blog: 'Blog',
|
||||
pricing: 'Pricing',
|
||||
contact: 'Contact',
|
||||
launchApp: 'Launch App',
|
||||
},
|
||||
hero: {
|
||||
@@ -162,17 +161,6 @@ export const translations = {
|
||||
proFeatures: ['Unlimited uploads', 'All export formats', 'Priority processing', 'API access'],
|
||||
enterpriseFeatures: ['Custom volume', 'Dedicated support', 'SLA guarantee', 'On-premise option'],
|
||||
},
|
||||
contact: {
|
||||
title: 'Contact Us',
|
||||
subtitle: 'Get in touch with our team',
|
||||
nameLabel: 'Name',
|
||||
emailLabel: 'Email',
|
||||
messageLabel: 'Message',
|
||||
send: 'Send Message',
|
||||
sending: 'Sending...',
|
||||
sent: 'Message sent!',
|
||||
qqGroup: 'QQ Group',
|
||||
},
|
||||
footer: {
|
||||
tagline: 'AI-powered math formula recognition',
|
||||
product: 'Product',
|
||||
@@ -294,7 +282,6 @@ export const translations = {
|
||||
docs: '文档',
|
||||
blog: '博客',
|
||||
pricing: '价格',
|
||||
contact: '联系我们',
|
||||
launchApp: '启动应用',
|
||||
},
|
||||
hero: {
|
||||
@@ -344,17 +331,6 @@ export const translations = {
|
||||
proFeatures: ['无限上传', '所有导出格式', '优先处理', 'API 访问'],
|
||||
enterpriseFeatures: ['自定义用量', '专属支持', 'SLA 保障', '私有部署选项'],
|
||||
},
|
||||
contact: {
|
||||
title: '联系我们',
|
||||
subtitle: '与我们的团队取得联系',
|
||||
nameLabel: '姓名',
|
||||
emailLabel: '邮箱',
|
||||
messageLabel: '留言',
|
||||
send: '发送消息',
|
||||
sending: '发送中...',
|
||||
sent: '消息已发送!',
|
||||
qqGroup: 'QQ 群',
|
||||
},
|
||||
footer: {
|
||||
tagline: 'AI 驱动的数学公式识别',
|
||||
product: '产品',
|
||||
|
||||
@@ -1,13 +1,17 @@
|
||||
import SEOHead from '../components/seo/SEOHead';
|
||||
import HeroSection from '../components/home/HeroSection';
|
||||
import ProductSuiteSection from '../components/home/ProductSuiteSection';
|
||||
import FeaturesSection from '../components/home/FeaturesSection';
|
||||
import HowItWorksSection from '../components/home/HowItWorksSection';
|
||||
import ShowcaseSection from '../components/home/ShowcaseSection';
|
||||
import TestimonialsSection from '../components/home/TestimonialsSection';
|
||||
import PricingSection from '../components/home/PricingSection';
|
||||
import ContactSection from '../components/home/ContactSection';
|
||||
import DocsSeoSection from '../components/home/DocsSeoSection';
|
||||
import { useScrollReveal } from '../hooks/useScrollReveal';
|
||||
import { useLanguage } from '../contexts/LanguageContext';
|
||||
|
||||
export default function HomePage() {
|
||||
const { t } = useLanguage();
|
||||
useScrollReveal();
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -17,10 +21,16 @@ export default function HomePage() {
|
||||
path="/"
|
||||
/>
|
||||
<HeroSection />
|
||||
<div className="section-divider" />
|
||||
<ProductSuiteSection />
|
||||
<FeaturesSection />
|
||||
<HowItWorksSection />
|
||||
<ShowcaseSection />
|
||||
<div className="section-divider" />
|
||||
<TestimonialsSection />
|
||||
<div className="section-divider" />
|
||||
<PricingSection />
|
||||
<ContactSection />
|
||||
<div className="section-divider" />
|
||||
<DocsSeoSection />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
1573
src/styles/landing.css
Normal file
1573
src/styles/landing.css
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user