diff --git a/src/pages/BlogDetailPage.tsx b/src/pages/BlogDetailPage.tsx index 5487436..c599603 100644 --- a/src/pages/BlogDetailPage.tsx +++ b/src/pages/BlogDetailPage.tsx @@ -1,36 +1,78 @@ import { useEffect, useState } from 'react'; import { useParams, Link } from 'react-router-dom'; -import { ArrowLeft, Calendar } from 'lucide-react'; import SEOHead from '../components/seo/SEOHead'; import { useLanguage } from '../contexts/LanguageContext'; import { loadContent, type ContentItem } from '../lib/content'; +function estimateReadTime(html: string): number { + const text = html.replace(/<[^>]+>/g, ''); + const words = text.split(/\s+/).filter(Boolean).length; + return Math.max(2, Math.round(words / 200)); +} + +function formatDate(dateStr: string, lang: string): string { + const d = new Date(dateStr); + if (isNaN(d.getTime())) return dateStr; + return d.toLocaleDateString(lang === 'zh' ? 'zh-CN' : 'en-US', { + year: 'numeric', + month: 'long', + day: 'numeric', + }); +} + export default function BlogDetailPage() { const { slug } = useParams<{ slug: string }>(); const { language } = useLanguage(); const [content, setContent] = useState(null); + const [notFound, setNotFound] = useState(false); useEffect(() => { + setContent(null); + setNotFound(false); if (slug) { loadContent('blog', language, slug) .then(setContent) - .catch(() => setContent(null)); + .catch(() => setNotFound(true)); } }, [slug, language]); - if (!content) { + const zh = language === 'zh'; + + if (notFound) { return ( -
-
-
-
-
-
+
+ + + {zh ? '所有文章' : 'All posts'} + +
+

+ {zh ? '文章未找到' : 'Post not found'} +

+ + {zh ? '返回博客 →' : 'Back to blog →'} +
); } + if (!content) { + return ( +
+
+
+
+
+
+
+
+
+ ); + } + + const readTime = estimateReadTime(content.html); + return ( <> -
- {/* Back link */} - - - {language === 'en' ? 'All posts' : '所有文章'} + +
+ {/* Back */} + + + {zh ? '所有文章' : 'All posts'} {/* Article header */} -
-
- - -
- {content.meta.tags && content.meta.tags.length > 0 && ( -
- {content.meta.tags.map((tag: string) => ( - - {tag} - +
+ {content.meta.tags.length > 0 && ( +
+ {content.meta.tags.map(tag => ( + {tag} ))}
)} -
+

{content.meta.title}

+
+ {formatDate(content.meta.date, language)} + · + {readTime} {zh ? '分钟阅读' : 'min read'} +
+
{/* Article body */} -
+ + {/* CTA */} +
+
+ {zh ? '准备好试试了吗?' : 'Ready to try it yourself?'} +
+

+ {zh + ? '上传一张公式图片,秒级获得 LaTeX 输出——无需注册。' + : 'Upload a formula image and get LaTeX output in under a second — no sign-up needed.'} +

+ + + + + {zh ? '免费试用 TexPixel' : 'Try TexPixel Free'} + +
); diff --git a/src/pages/BlogListPage.tsx b/src/pages/BlogListPage.tsx index 4b960be..99ebea6 100644 --- a/src/pages/BlogListPage.tsx +++ b/src/pages/BlogListPage.tsx @@ -1,10 +1,19 @@ import { useEffect, useState } from 'react'; import { Link } from 'react-router-dom'; -import { ArrowRight, Calendar } from 'lucide-react'; import SEOHead from '../components/seo/SEOHead'; import { useLanguage } from '../contexts/LanguageContext'; import { loadManifest, type ContentMeta } from '../lib/content'; +function formatDate(dateStr: string, lang: string): string { + const d = new Date(dateStr); + if (isNaN(d.getTime())) return dateStr; + return d.toLocaleDateString(lang === 'zh' ? 'zh-CN' : 'en-US', { + year: 'numeric', + month: 'long', + day: 'numeric', + }); +} + export default function BlogListPage() { const { language } = useLanguage(); const [posts, setPosts] = useState([]); @@ -15,80 +24,71 @@ export default function BlogListPage() { }); }, [language]); + const zh = language === 'zh'; const featured = posts[0]; const rest = posts.slice(1); return ( <> - + -
+
{/* Header */} -
-

- {language === 'en' ? 'Blog' : '博客'} +
+
{zh ? '博客' : 'Blog'}
+

+ {zh ? '最新文章' : 'Latest Posts'}

-

- {language === 'en' - ? 'Updates, tutorials, and insights on formula recognition and LaTeX.' - : '关于公式识别和 LaTeX 的更新、教程和见解。'} +

+ {zh + ? '关于公式识别和 LaTeX 的更新、教程与见解。' + : 'Updates, tutorials, and insights on formula recognition and LaTeX.'}

{/* Featured post */} {featured && ( - -
- - + +
+ {zh ? '精选文章' : 'Featured'}
-

- {featured.title} -

-

- {featured.description} -

-
-
- {featured.tags.map(tag => ( - - {tag} - +
{featured.title}
+
{featured.description}
+
+
+ {featured.tags.slice(0, 3).map(tag => ( + {tag} ))} + + {formatDate(featured.date, language)} +
- - {language === 'en' ? 'Read more' : '阅读全文'} - + + {zh ? '阅读全文' : 'Read more'} + + +
)} - {/* Rest of posts */} + {/* Post grid */} {rest.length > 0 && ( -
- {rest.map((post) => ( - -
- -
-

- {post.title} -

-

{post.description}

+
+ {rest.map(post => ( + +
{formatDate(post.date, language)}
+
{post.title}
+
{post.description}
{post.tags.length > 0 && ( -
- {post.tags.map(tag => ( - - {tag} - +
+ {post.tags.slice(0, 2).map(tag => ( + {tag} ))}
)} @@ -99,10 +99,8 @@ export default function BlogListPage() { {/* Empty state */} {posts.length === 0 && ( -
-

- {language === 'en' ? 'No posts yet. Check back soon!' : '暂无文章,敬请期待!'} -

+
+ {zh ? '暂无文章,敬请期待。' : 'No posts yet. Check back soon.'}
)}
diff --git a/src/styles/landing.css b/src/styles/landing.css index 011b9d1..0665620 100644 --- a/src/styles/landing.css +++ b/src/styles/landing.css @@ -28,6 +28,7 @@ } .marketing-page { + position: relative; background: var(--bg); color: var(--text-strong); font-family: 'DM Sans', sans-serif; @@ -39,7 +40,7 @@ /* ── GRID BACKGROUND ── */ .marketing-page::before { content: ''; - position: fixed; + position: absolute; inset: 0; background-image: linear-gradient(var(--border) 1px, transparent 1px), @@ -51,27 +52,33 @@ } /* ── GLOW BLOBS ── */ +/* position: fixed keeps blobs in-viewport at all times. + will-change + translateZ promotes each blob to its own GPU compositor + layer — the browser composites them independently so main-thread scroll + repaints do NOT include these elements. */ .glow-blob { position: fixed; border-radius: 50%; filter: blur(80px); pointer-events: none; z-index: 0; + will-change: transform; + transform: translateZ(0); } .glow-blob-1 { - width: 480px; height: 380px; - background: rgba(200,98,42,0.09); - top: -80px; right: 80px; + width: 520px; height: 400px; + background: radial-gradient(circle, rgba(200,98,42,0.13) 0%, transparent 70%); + top: -100px; right: 60px; } .glow-blob-2 { - width: 360px; height: 320px; - background: rgba(140,201,190,0.10); - bottom: 20%; left: -80px; + width: 400px; height: 360px; + background: radial-gradient(circle, rgba(140,201,190,0.13) 0%, transparent 70%); + bottom: 25%; left: -100px; } .glow-blob-3 { - width: 320px; height: 280px; - background: rgba(243,201,106,0.08); - top: 55%; right: -60px; + width: 360px; height: 300px; + background: radial-gradient(circle, rgba(243,201,106,0.11) 0%, transparent 70%); + top: 50%; right: -80px; } /* ── LAYOUT ── */ @@ -1970,3 +1977,190 @@ 0%, 100% { opacity: 0.7; } 50% { opacity: 0.35; } } + +/* ════════════════════════════════════════ + BLOG PAGES + ════════════════════════════════════════ */ + +/* ── Blog list page ── */ +.blog-page { + max-width: 900px; + margin: 0 auto; + padding: 64px 24px 80px; +} + +.blog-page-header { + text-align: center; + margin-bottom: 48px; +} + +.blog-page-title { + font-family: 'Lora', serif; + font-size: clamp(2rem, 4vw, 2.75rem); + font-weight: 700; + color: var(--text-strong); + line-height: 1.2; + margin-bottom: 12px; +} + +.blog-page-subtitle { + font-size: 1.0625rem; + color: var(--text-muted); + max-width: 480px; + margin: 0 auto; +} + +/* ── Featured post ── */ +.blog-featured { + display: block; + background: var(--card); + border: 1.5px solid var(--border); + border-radius: var(--r-l); + padding: 36px 40px; + margin-bottom: 32px; + text-decoration: none; + position: relative; + overflow: hidden; + transition: box-shadow 0.18s, border-color 0.18s, transform 0.18s; +} + +.blog-featured::before { + content: ''; + position: absolute; + inset: 0; + background: linear-gradient(135deg, rgba(200,98,42,0.04) 0%, transparent 60%); + pointer-events: none; +} + +.blog-featured:hover { + border-color: var(--warm-wash); + box-shadow: var(--shadow-float); + transform: translateY(-2px); +} + +.blog-featured-eyebrow { + display: inline-block; + font-size: 0.75rem; + font-weight: 600; + letter-spacing: 0.08em; + text-transform: uppercase; + color: var(--primary); + margin-bottom: 14px; +} + +.blog-featured-title { + font-family: 'Lora', serif; + font-size: clamp(1.375rem, 2.5vw, 1.875rem); + font-weight: 700; + color: var(--text-strong); + line-height: 1.3; + margin-bottom: 12px; + transition: color 0.15s; +} + +.blog-featured:hover .blog-featured-title { + color: var(--primary); +} + +.blog-featured-desc { + font-size: 1rem; + color: var(--text-body); + line-height: 1.7; + margin-bottom: 20px; + max-width: 680px; +} + +.blog-featured-footer { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + flex-wrap: wrap; +} + +.blog-featured-read { + font-size: 0.875rem; + font-weight: 600; + color: var(--primary); + display: inline-flex; + align-items: center; + gap: 4px; + transition: gap 0.15s; +} + +.blog-featured:hover .blog-featured-read { + gap: 8px; +} + +/* ── Blog grid ── */ +.blog-grid { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 20px; +} + +.blog-card { + display: block; + background: var(--card); + border: 1.5px solid var(--border); + border-radius: var(--r-m); + padding: 24px 28px; + text-decoration: none; + transition: box-shadow 0.18s, border-color 0.18s, transform 0.18s; +} + +.blog-card:hover { + border-color: var(--warm-wash); + box-shadow: var(--shadow-soft); + transform: translateY(-2px); +} + +.blog-card-date { + font-size: 0.75rem; + color: var(--text-muted); + margin-bottom: 10px; +} + +.blog-card-title { + font-family: 'Lora', serif; + font-size: 1.0625rem; + font-weight: 600; + color: var(--text-strong); + line-height: 1.4; + margin-bottom: 8px; + transition: color 0.15s; +} + +.blog-card:hover .blog-card-title { + color: var(--primary); +} + +.blog-card-desc { + font-size: 0.875rem; + color: var(--text-body); + line-height: 1.65; + margin-bottom: 16px; +} + +.blog-card-footer { + display: flex; + align-items: center; + gap: 6px; + flex-wrap: wrap; +} + +.blog-empty { + text-align: center; + padding: 64px 0; + color: var(--text-muted); + font-size: 0.9375rem; +} + +@media (max-width: 640px) { + .blog-grid { grid-template-columns: 1fr; } + .blog-featured { padding: 24px; } + .blog-page { padding: 40px 16px 64px; } +} + +/* ── Blog detail page (reuses .docs-detail, .docs-back-link, + .docs-prose, .docs-cta-box, .docs-skeleton-wrap) ── */