- Rewrote DocsListPage and DocDetailPage with landing.css aesthetic (icon cards, skeleton loader, prose styles, CTA box) - Added docs-specific CSS to landing.css - Created image-to-latex, copy-to-word, ocr-accuracy, pdf-extraction articles in both English and Chinese - Updated DocsSeoSection guide cards to link to real doc slugs Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1524 lines
54 KiB
Markdown
1524 lines
54 KiB
Markdown
# Landing Page Refactor Implementation Plan
|
||
|
||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||
|
||
**Goal:** Replace all marketing home components with the UI from `texpixel-landing.html`, preserving exact visual fidelity while integrating with React Router, AuthContext, and existing app infrastructure.
|
||
|
||
**Architecture:** Extract the reference HTML's `<style>` block into a scoped `landing.css` (imported only in `MarketingLayout`), convert each HTML section into a focused React component, and convert the reference's `<script>` behaviors into `useEffect` hooks and a shared `useScrollReveal` hook.
|
||
|
||
**Tech Stack:** React 18, TypeScript, React Router v6, Tailwind CSS (workspace only), custom CSS (marketing only), `useAuth` / `useLanguage` contexts.
|
||
|
||
**Spec:** `docs/superpowers/specs/2026-03-26-landing-refactor-design.md`
|
||
**Reference:** `texpixel-landing.html` (lines 10-1593 = CSS, lines 1602-2183 = HTML, lines 2185-2289 = JS)
|
||
|
||
---
|
||
|
||
## File Map
|
||
|
||
### Create
|
||
- `src/styles/landing.css` — all marketing CSS, scoped under `.marketing-page`
|
||
- `src/hooks/useScrollReveal.ts` — IntersectionObserver for `.reveal` elements
|
||
- `src/components/home/ProductSuiteSection.tsx` — `.product-suite` section
|
||
- `src/components/home/ShowcaseSection.tsx` — `.showcase` section
|
||
- `src/components/home/TestimonialsSection.tsx` — `.user-love` carousel section
|
||
- `src/components/home/DocsSeoSection.tsx` — `.docs-seo` section
|
||
|
||
### Modify
|
||
- `index.html` — add Lora + JetBrains Mono fonts
|
||
- `src/components/layout/MarketingLayout.tsx` — import CSS, add `.marketing-page` wrapper + glow blobs
|
||
- `src/components/layout/MarketingNavbar.tsx` — replace with reference nav design
|
||
- `src/components/layout/Footer.tsx` — replace with reference footer design
|
||
- `src/components/home/HeroSection.tsx` — replace with reference hero + typing effect
|
||
- `src/components/home/FeaturesSection.tsx` — replace with reference core features
|
||
- `src/components/home/PricingSection.tsx` — replace with reference pricing
|
||
- `src/pages/HomePage.tsx` — update section order, add new sections, add scroll reveal
|
||
- `src/lib/translations.ts` — remove dead `contact` key from `marketing.nav`
|
||
|
||
### Delete
|
||
- `src/components/home/HowItWorksSection.tsx`
|
||
- `src/components/home/ContactSection.tsx`
|
||
|
||
---
|
||
|
||
## Task 1: Add Fonts to index.html
|
||
|
||
**Files:**
|
||
- Modify: `index.html:15`
|
||
|
||
- [ ] **Step 1: Add Lora and JetBrains Mono to the Google Fonts link**
|
||
|
||
Replace the existing fonts line (line 15) in `index.html`:
|
||
```html
|
||
<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" />
|
||
```
|
||
With:
|
||
```html
|
||
<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" />
|
||
```
|
||
|
||
- [ ] **Step 2: Verify build still passes**
|
||
|
||
```bash
|
||
npm run typecheck
|
||
```
|
||
Expected: no errors
|
||
|
||
- [ ] **Step 3: Commit**
|
||
|
||
```bash
|
||
git add index.html
|
||
git commit -m "feat: add Lora and JetBrains Mono fonts for landing page"
|
||
```
|
||
|
||
---
|
||
|
||
## Task 2: Extract Landing CSS
|
||
|
||
**Files:**
|
||
- Create: `src/styles/landing.css`
|
||
|
||
- [ ] **Step 1: Create the styles directory and file**
|
||
|
||
```bash
|
||
mkdir -p src/styles
|
||
```
|
||
|
||
- [ ] **Step 2: Copy the CSS from the reference and apply scoping**
|
||
|
||
Copy lines 10–1593 from `texpixel-landing.html` (everything inside the `<style>` tag) into `src/styles/landing.css`.
|
||
|
||
Then apply these transformations to prevent workspace bleed:
|
||
|
||
| Original selector | Replace with |
|
||
|---|---|
|
||
| `body {` | `.marketing-page {` |
|
||
| `body::before {` | `.marketing-page::before {` |
|
||
| `html { scroll-behavior: smooth; }` | Remove this line (already in `index.css` via Tailwind base) |
|
||
| `section { position: relative; z-index: 1; }` | `.marketing-page section { position: relative; z-index: 1; }` |
|
||
| `nav {` | `.marketing-page nav {` |
|
||
| `footer {` (the footer padding rule) | `.marketing-page footer {` |
|
||
|
||
Leave `:root { ... }` untouched — the variable names (`--primary`, `--bg`, etc.) don't conflict with the workspace's variables (`--color-primary`, `--color-bg`).
|
||
|
||
All class-based selectors (`.hero`, `.btn`, `.product-card`, etc.) need no change — they are unique enough to not affect the workspace.
|
||
|
||
At the very end of the file, add the scroll reveal animation rule used by JS:
|
||
```css
|
||
.reveal {
|
||
opacity: 0;
|
||
transform: translateY(24px);
|
||
transition: opacity 0.55s ease, transform 0.55s ease;
|
||
}
|
||
.reveal.visible {
|
||
opacity: 1;
|
||
transform: translateY(0);
|
||
}
|
||
.reveal-delay-1 { transition-delay: 0.10s; }
|
||
.reveal-delay-2 { transition-delay: 0.20s; }
|
||
.reveal-delay-3 { transition-delay: 0.30s; }
|
||
```
|
||
|
||
- [ ] **Step 3: Verify the file exists**
|
||
|
||
```bash
|
||
ls src/styles/landing.css
|
||
```
|
||
Expected: file listed
|
||
|
||
- [ ] **Step 4: Commit**
|
||
|
||
```bash
|
||
git add src/styles/landing.css
|
||
git commit -m "feat: extract landing page CSS from reference HTML"
|
||
```
|
||
|
||
---
|
||
|
||
## Task 3: Update MarketingLayout
|
||
|
||
**Files:**
|
||
- Modify: `src/components/layout/MarketingLayout.tsx`
|
||
|
||
- [ ] **Step 1: Replace MarketingLayout.tsx**
|
||
|
||
```tsx
|
||
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="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>
|
||
<Outlet />
|
||
</main>
|
||
<Footer />
|
||
</div>
|
||
);
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 2: Verify typecheck passes**
|
||
|
||
```bash
|
||
npm run typecheck
|
||
```
|
||
Expected: no errors
|
||
|
||
- [ ] **Step 3: Commit**
|
||
|
||
```bash
|
||
git add src/components/layout/MarketingLayout.tsx
|
||
git commit -m "feat: wire landing.css into MarketingLayout with .marketing-page scope"
|
||
```
|
||
|
||
---
|
||
|
||
## Task 4: Create useScrollReveal Hook
|
||
|
||
**Files:**
|
||
- Create: `src/hooks/useScrollReveal.ts`
|
||
|
||
- [ ] **Step 1: Create hooks directory and hook file**
|
||
|
||
```typescript
|
||
// src/hooks/useScrollReveal.ts
|
||
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();
|
||
}, []);
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 2: Verify typecheck**
|
||
|
||
```bash
|
||
npm run typecheck
|
||
```
|
||
Expected: no errors
|
||
|
||
- [ ] **Step 3: Commit**
|
||
|
||
```bash
|
||
git add src/hooks/useScrollReveal.ts
|
||
git commit -m "feat: add useScrollReveal hook for intersection-based fade-in"
|
||
```
|
||
|
||
---
|
||
|
||
## Task 5: Replace MarketingNavbar
|
||
|
||
**Files:**
|
||
- Modify: `src/components/layout/MarketingNavbar.tsx`
|
||
|
||
The reference navbar has: sticky bar, SVG logo, nav links, lang switch, user avatar dropdown (auth-aware), CTA button.
|
||
|
||
- [ ] **Step 1: Replace MarketingNavbar.tsx**
|
||
|
||
```tsx
|
||
import { useState, useEffect, useRef } from 'react';
|
||
import { Link, useLocation } from 'react-router-dom';
|
||
import { useLanguage } from '../../contexts/LanguageContext';
|
||
import { useAuth } from '../../contexts/AuthContext';
|
||
|
||
export default function MarketingNavbar() {
|
||
const { language, setLanguage } = useLanguage();
|
||
const { user, signOut } = useAuth();
|
||
const location = useLocation();
|
||
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 = [
|
||
{ href: '/', label: language === 'zh' ? '首页' : 'Home', isRouter: true },
|
||
{ href: '/docs', label: language === 'zh' ? '文档' : 'Docs', isRouter: true },
|
||
{ href: '/blog', label: language === 'zh' ? '博客' : 'Blog', isRouter: true },
|
||
];
|
||
|
||
const anchorLinks = isHome
|
||
? [{ href: '#pricing', label: language === 'zh' ? '价格' : 'Pricing' }]
|
||
: [];
|
||
|
||
return (
|
||
<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>
|
||
|
||
<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="nav-right">
|
||
<button
|
||
className="lang-switch"
|
||
onClick={() => setLanguage(language === 'zh' ? 'en' : 'zh')}
|
||
>
|
||
{language === 'zh' ? 'EN' : '中文'}
|
||
</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>
|
||
</div>
|
||
</nav>
|
||
);
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 2: Verify typecheck**
|
||
|
||
```bash
|
||
npm run typecheck
|
||
```
|
||
Expected: no errors
|
||
|
||
- [ ] **Step 3: Commit**
|
||
|
||
```bash
|
||
git add src/components/layout/MarketingNavbar.tsx
|
||
git commit -m "feat: replace MarketingNavbar with reference design"
|
||
```
|
||
|
||
---
|
||
|
||
## Task 6: Replace HeroSection
|
||
|
||
**Files:**
|
||
- Modify: `src/components/home/HeroSection.tsx`
|
||
|
||
The hero has: left text column, right mock window with upload zone + typing LaTeX output.
|
||
|
||
- [ ] **Step 1: Replace HeroSection.tsx**
|
||
|
||
```tsx
|
||
import { useEffect, useRef } from 'react';
|
||
import { Link } from 'react-router-dom';
|
||
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 { 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="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="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="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="#products" className="btn btn-secondary">
|
||
{language === 'zh' ? '了解更多 →' : 'Learn More →'}
|
||
</a>
|
||
</div>
|
||
|
||
<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>
|
||
</div>
|
||
</div>
|
||
|
||
<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="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>
|
||
|
||
<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>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
);
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 2: Verify typecheck**
|
||
|
||
```bash
|
||
npm run typecheck
|
||
```
|
||
Expected: no errors
|
||
|
||
- [ ] **Step 3: Commit**
|
||
|
||
```bash
|
||
git add src/components/home/HeroSection.tsx
|
||
git commit -m "feat: replace HeroSection with reference design + typing effect"
|
||
```
|
||
|
||
---
|
||
|
||
## Task 7: Create ProductSuiteSection
|
||
|
||
**Files:**
|
||
- Create: `src/components/home/ProductSuiteSection.tsx`
|
||
|
||
- [ ] **Step 1: Create ProductSuiteSection.tsx**
|
||
|
||
```tsx
|
||
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>
|
||
);
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 2: Verify typecheck**
|
||
|
||
```bash
|
||
npm run typecheck
|
||
```
|
||
Expected: no errors
|
||
|
||
- [ ] **Step 3: Commit**
|
||
|
||
```bash
|
||
git add src/components/home/ProductSuiteSection.tsx
|
||
git commit -m "feat: add ProductSuiteSection"
|
||
```
|
||
|
||
---
|
||
|
||
## Task 8: Replace FeaturesSection (Core Features)
|
||
|
||
**Files:**
|
||
- Modify: `src/components/home/FeaturesSection.tsx`
|
||
|
||
- [ ] **Step 1: Replace FeaturesSection.tsx**
|
||
|
||
```tsx
|
||
import { useLanguage } from '../../contexts/LanguageContext';
|
||
|
||
export default function FeaturesSection() {
|
||
const { language } = useLanguage();
|
||
|
||
return (
|
||
<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="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>
|
||
);
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 2: Verify typecheck**
|
||
|
||
```bash
|
||
npm run typecheck
|
||
```
|
||
|
||
- [ ] **Step 3: Commit**
|
||
|
||
```bash
|
||
git add src/components/home/FeaturesSection.tsx
|
||
git commit -m "feat: replace FeaturesSection with reference core features design"
|
||
```
|
||
|
||
---
|
||
|
||
## Task 9: Create ShowcaseSection
|
||
|
||
**Files:**
|
||
- Create: `src/components/home/ShowcaseSection.tsx`
|
||
|
||
- [ ] **Step 1: Create ShowcaseSection.tsx**
|
||
|
||
```tsx
|
||
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>
|
||
);
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 2: Verify typecheck**
|
||
|
||
```bash
|
||
npm run typecheck
|
||
```
|
||
|
||
- [ ] **Step 3: Commit**
|
||
|
||
```bash
|
||
git add src/components/home/ShowcaseSection.tsx
|
||
git commit -m "feat: add ShowcaseSection with formula examples"
|
||
```
|
||
|
||
---
|
||
|
||
## Task 10: Create TestimonialsSection (Carousel)
|
||
|
||
**Files:**
|
||
- Create: `src/components/home/TestimonialsSection.tsx`
|
||
|
||
- [ ] **Step 1: Create TestimonialsSection.tsx**
|
||
|
||
6 testimonials, 3 visible at a time = 4 pages. Carousel uses CSS `transform: translateX` on a flex track.
|
||
|
||
```tsx
|
||
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>
|
||
);
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 2: Verify typecheck**
|
||
|
||
```bash
|
||
npm run typecheck
|
||
```
|
||
Expected: no errors
|
||
|
||
- [ ] **Step 3: Commit**
|
||
|
||
```bash
|
||
git add src/components/home/TestimonialsSection.tsx
|
||
git commit -m "feat: add TestimonialsSection with auto-advancing carousel"
|
||
```
|
||
|
||
---
|
||
|
||
## Task 11: Replace PricingSection
|
||
|
||
**Files:**
|
||
- Modify: `src/components/home/PricingSection.tsx`
|
||
|
||
- [ ] **Step 1: Replace PricingSection.tsx**
|
||
|
||
```tsx
|
||
import { Link } from 'react-router-dom';
|
||
import { useLanguage } from '../../contexts/LanguageContext';
|
||
|
||
export default function PricingSection() {
|
||
const { language } = useLanguage();
|
||
const zh = language === 'zh';
|
||
|
||
return (
|
||
<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="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>
|
||
|
||
<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>
|
||
|
||
<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>
|
||
|
||
<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>
|
||
);
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 2: Verify typecheck**
|
||
|
||
```bash
|
||
npm run typecheck
|
||
```
|
||
|
||
- [ ] **Step 3: Commit**
|
||
|
||
```bash
|
||
git add src/components/home/PricingSection.tsx
|
||
git commit -m "feat: replace PricingSection with reference 4-tier pricing design"
|
||
```
|
||
|
||
---
|
||
|
||
## Task 12: Create DocsSeoSection
|
||
|
||
**Files:**
|
||
- Create: `src/components/home/DocsSeoSection.tsx`
|
||
|
||
- [ ] **Step 1: Create DocsSeoSection.tsx**
|
||
|
||
```tsx
|
||
import { Link } from 'react-router-dom';
|
||
import { useLanguage } from '../../contexts/LanguageContext';
|
||
|
||
const GUIDES = [
|
||
{
|
||
icon: <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/>,
|
||
icon2: <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 分钟 · 最受欢迎',
|
||
},
|
||
{
|
||
icon: <><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 分钟 · 扩展用户',
|
||
},
|
||
{
|
||
icon: <><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 分钟 · 进阶用户',
|
||
},
|
||
{
|
||
icon: <><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.icon}
|
||
{g.icon2}
|
||
</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>
|
||
);
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 2: Verify typecheck**
|
||
|
||
```bash
|
||
npm run typecheck
|
||
```
|
||
|
||
- [ ] **Step 3: Commit**
|
||
|
||
```bash
|
||
git add src/components/home/DocsSeoSection.tsx
|
||
git commit -m "feat: add DocsSeoSection with guide cards"
|
||
```
|
||
|
||
---
|
||
|
||
## Task 13: Replace Footer
|
||
|
||
**Files:**
|
||
- Modify: `src/components/layout/Footer.tsx`
|
||
|
||
- [ ] **Step 1: Replace Footer.tsx**
|
||
|
||
```tsx
|
||
import { Link } from 'react-router-dom';
|
||
import { useLanguage } from '../../contexts/LanguageContext';
|
||
|
||
export default function Footer() {
|
||
const { language } = useLanguage();
|
||
const zh = language === 'zh';
|
||
|
||
return (
|
||
<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>
|
||
|
||
<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>
|
||
|
||
<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>
|
||
|
||
<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="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>
|
||
</footer>
|
||
);
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 2: Verify typecheck**
|
||
|
||
```bash
|
||
npm run typecheck
|
||
```
|
||
|
||
- [ ] **Step 3: Commit**
|
||
|
||
```bash
|
||
git add src/components/layout/Footer.tsx
|
||
git commit -m "feat: replace Footer with reference design"
|
||
```
|
||
|
||
---
|
||
|
||
## Task 14: Update HomePage + Delete Old Components + Clean Translations
|
||
|
||
**Files:**
|
||
- Modify: `src/pages/HomePage.tsx`
|
||
- Modify: `src/lib/translations.ts`
|
||
- Delete: `src/components/home/HowItWorksSection.tsx`
|
||
- Delete: `src/components/home/ContactSection.tsx`
|
||
|
||
- [ ] **Step 1: Replace HomePage.tsx**
|
||
|
||
```tsx
|
||
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 ShowcaseSection from '../components/home/ShowcaseSection';
|
||
import TestimonialsSection from '../components/home/TestimonialsSection';
|
||
import PricingSection from '../components/home/PricingSection';
|
||
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 (
|
||
<>
|
||
<SEOHead
|
||
title="TexPixel - AI Math Formula Recognition | LaTeX, MathML OCR Tool"
|
||
description={t.marketing.hero.subtitle}
|
||
path="/"
|
||
/>
|
||
<HeroSection />
|
||
<div className="section-divider" />
|
||
<ProductSuiteSection />
|
||
<FeaturesSection />
|
||
<ShowcaseSection />
|
||
<div className="section-divider" />
|
||
<TestimonialsSection />
|
||
<div className="section-divider" />
|
||
<PricingSection />
|
||
<div className="section-divider" />
|
||
<DocsSeoSection />
|
||
</>
|
||
);
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 2: Remove dead `contact` keys from translations.ts**
|
||
|
||
In `src/lib/translations.ts`:
|
||
- Remove `contact: 'Contact',` from the `en.marketing.nav` block (around line 115)
|
||
- Remove `contact: '联系我们',` from the `zh.marketing.nav` block (around line 297)
|
||
- Remove the entire `en.marketing.contact` block (the object starting with `contact: { title:` around line 165–175)
|
||
- Remove the entire `zh.marketing.contact` block (equivalent block in the zh section)
|
||
|
||
- [ ] **Step 3: Delete old components**
|
||
|
||
```bash
|
||
rm src/components/home/HowItWorksSection.tsx
|
||
rm src/components/home/ContactSection.tsx
|
||
```
|
||
|
||
- [ ] **Step 4: Verify typecheck and build**
|
||
|
||
```bash
|
||
npm run typecheck && npm run build
|
||
```
|
||
Expected: no TypeScript errors, build succeeds
|
||
|
||
- [ ] **Step 5: Commit**
|
||
|
||
```bash
|
||
git add src/pages/HomePage.tsx src/lib/translations.ts
|
||
git rm src/components/home/HowItWorksSection.tsx src/components/home/ContactSection.tsx
|
||
git commit -m "feat: wire all landing sections in HomePage, remove obsolete components"
|
||
```
|
||
|
||
---
|
||
|
||
## Task 15: Verify CSS Scoping Does Not Break Workspace
|
||
|
||
**Files:**
|
||
- No code changes — verification only
|
||
|
||
- [ ] **Step 1: Run the dev server and navigate to /app**
|
||
|
||
```bash
|
||
npm run dev
|
||
```
|
||
|
||
Open `http://localhost:5173/app` — verify:
|
||
- The workspace `AppLayout` and `WorkspacePage` render correctly
|
||
- No `.marketing-page` CSS rules leak (no warm beige background, no grid overlay on the workspace)
|
||
|
||
- [ ] **Step 2: Navigate to / (home)**
|
||
|
||
Open `http://localhost:5173/` — verify:
|
||
- Background matches reference (`#FFFBF7` warm cream with grid pattern)
|
||
- All three glow blobs visible as ambient background
|
||
- Navbar, Hero, all sections, and Footer match the reference HTML visually
|
||
|
||
- [ ] **Step 3: Run full build**
|
||
|
||
```bash
|
||
npm run build
|
||
```
|
||
Expected: success with no errors
|
||
|
||
- [ ] **Step 4: Final commit**
|
||
|
||
```bash
|
||
git add -A
|
||
git commit -m "chore: final verification — landing refactor complete"
|
||
```
|