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:
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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user