fix: resolve Web UI display issues (#365)

## Fixes
### 1. Typewriter Component - Missing First Character
- Fix character loss issue where first character of each line was missing
- Add proper state reset logic before starting typing animation
- Extract character before setState to avoid closure issues
- Add setTimeout(0) to ensure state is updated before typing starts
- Change dependency from `lines` to `sanitizedLines` for correct updates
- Use `??` instead of `||` for safer null handling
### 2. Chinese Translation - Leading Spaces
- Remove leading spaces from startupMessages1/2/3 in Chinese translations
- Ensures proper display of startup messages in terminal simulation
### 3. Dynamic GitHub Stats with Animation
- Add useGitHubStats hook to fetch real-time GitHub repository data
- Add useCounterAnimation hook with easeOutExpo easing for smooth number animation
- Display dynamic star count with smooth counter animation (2s duration)
- Display dynamic days count (static, no animation)
- Support bilingual display (EN/ZH) with proper formatting
## Changes
- web/src/components/Typewriter.tsx: Fix first character loss bug
- web/src/i18n/translations.ts: Remove leading spaces in Chinese messages
- web/src/components/landing/HeroSection.tsx: Add dynamic GitHub stats
- web/src/hooks/useGitHubStats.ts: New hook for GitHub API integration
- web/src/hooks/useCounterAnimation.ts: New hook for number animations
Fixes #365
Co-Authored-By: tinkle-community <tinklefund@gmail.com>
This commit is contained in:
Ember
2025-11-05 10:51:20 +08:00
parent 572bc3292d
commit d1f7ced7e1
5 changed files with 150 additions and 8 deletions

View File

@@ -24,13 +24,19 @@ export default function Typewriter({
const sanitizedLines = useMemo(() => lines.map((l) => String(l ?? '')), [lines])
useEffect(() => {
// 重置状态
lineIndexRef.current = 0
charIndexRef.current = 0
setTypedLines([''])
function typeNext() {
const currentLine = sanitizedLines[lineIndexRef.current] ?? ''
if (charIndexRef.current < currentLine.length) {
const ch = currentLine.charAt(charIndexRef.current)
setTypedLines((prev) => {
const next = [...prev]
const ch = currentLine.charAt(charIndexRef.current)
next[next.length - 1] = (next[next.length - 1] || '') + ch
const lastIndex = next.length - 1
next[lastIndex] = (next[lastIndex] ?? '') + ch
return next
})
charIndexRef.current += 1
@@ -49,7 +55,8 @@ export default function Typewriter({
}
}
typeNext()
// 延迟一帧开始打字,确保状态已重置
timerRef.current = window.setTimeout(typeNext, 0)
// 光标闪烁
blinkRef.current = window.setInterval(() => {
@@ -60,7 +67,7 @@ export default function Typewriter({
if (timerRef.current) window.clearTimeout(timerRef.current)
if (blinkRef.current) window.clearInterval(blinkRef.current)
}
}, [lines, typingSpeed, lineDelay])
}, [sanitizedLines, typingSpeed, lineDelay])
const displayText = useMemo(() => typedLines.join('\n').replace(/undefined/g, ''), [typedLines])

View File

@@ -1,6 +1,8 @@
import { motion, useScroll, useTransform, useAnimation } from 'framer-motion'
import { Sparkles } from 'lucide-react'
import { t, Language } from '../../i18n/translations'
import { useGitHubStats } from '../../hooks/useGitHubStats'
import { useCounterAnimation } from '../../hooks/useCounterAnimation'
interface HeroSectionProps {
language: Language
@@ -11,6 +13,14 @@ export default function HeroSection({ language }: HeroSectionProps) {
const opacity = useTransform(scrollYProgress, [0, 0.2], [1, 0])
const scale = useTransform(scrollYProgress, [0, 0.2], [1, 0.8])
const handControls = useAnimation()
const { stars, daysOld, isLoading } = useGitHubStats('NoFxAiOS', 'nofx')
// 动画数字 - 仅对 stars 添加动画
const animatedStars = useCounterAnimation({
start: 0,
end: stars,
duration: 2000,
})
const fadeInUp = {
initial: { opacity: 0, y: 60 },
@@ -33,7 +43,20 @@ export default function HeroSection({ language }: HeroSectionProps) {
>
<Sparkles className='w-4 h-4' style={{ color: 'var(--brand-yellow)' }} />
<span className='text-sm font-semibold' style={{ color: 'var(--brand-yellow)' }}>
{t('githubStarsInDays', language)}
{isLoading ? (
t('githubStarsInDays', language)
) : language === 'zh' ? (
<>
{daysOld} {' '}
<span className='inline-block tabular-nums'>{(animatedStars / 1000).toFixed(1)}</span>
K+ GitHub Stars
</>
) : (
<>
<span className='inline-block tabular-nums'>{(animatedStars / 1000).toFixed(1)}</span>
K+ GitHub Stars in {daysOld} days
</>
)}
</span>
</motion.div>
</motion.div>