Files
doc_ai_frontend/src/components/home/TestimonialsSection.tsx
yoge dceb775a1b 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>
2026-03-26 15:57:39 +08:00

184 lines
6.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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>
);
}