mirror of
https://github.com/NoFxAiOS/nofx.git
synced 2026-07-09 14:00:57 +08:00
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 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
51
web/src/hooks/useCounterAnimation.ts
Normal file
51
web/src/hooks/useCounterAnimation.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
|
||||
interface UseCounterAnimationOptions {
|
||||
start?: number
|
||||
end: number
|
||||
duration?: number
|
||||
decimals?: number
|
||||
}
|
||||
|
||||
export function useCounterAnimation({
|
||||
start = 0,
|
||||
end,
|
||||
duration = 2000,
|
||||
decimals = 0,
|
||||
}: UseCounterAnimationOptions): number {
|
||||
const [count, setCount] = useState(start)
|
||||
|
||||
useEffect(() => {
|
||||
if (end === 0) return
|
||||
|
||||
let startTime: number | null = null
|
||||
let animationFrame: number
|
||||
|
||||
const animate = (currentTime: number) => {
|
||||
if (startTime === null) startTime = currentTime
|
||||
const progress = Math.min((currentTime - startTime) / duration, 1)
|
||||
|
||||
// 使用 easeOutExpo 缓动函数,让数字快速启动后缓慢停止
|
||||
const easeOutExpo = progress === 1 ? 1 : 1 - Math.pow(2, -10 * progress)
|
||||
|
||||
const currentCount = start + (end - start) * easeOutExpo
|
||||
setCount(currentCount)
|
||||
|
||||
if (progress < 1) {
|
||||
animationFrame = requestAnimationFrame(animate)
|
||||
} else {
|
||||
setCount(end)
|
||||
}
|
||||
}
|
||||
|
||||
animationFrame = requestAnimationFrame(animate)
|
||||
|
||||
return () => {
|
||||
if (animationFrame) {
|
||||
cancelAnimationFrame(animationFrame)
|
||||
}
|
||||
}
|
||||
}, [start, end, duration])
|
||||
|
||||
return decimals > 0 ? parseFloat(count.toFixed(decimals)) : Math.floor(count)
|
||||
}
|
||||
61
web/src/hooks/useGitHubStats.ts
Normal file
61
web/src/hooks/useGitHubStats.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
|
||||
interface GitHubStats {
|
||||
stars: number
|
||||
forks: number
|
||||
createdAt: string
|
||||
daysOld: number
|
||||
isLoading: boolean
|
||||
error: string | null
|
||||
}
|
||||
|
||||
export function useGitHubStats(owner: string, repo: string): GitHubStats {
|
||||
const [stats, setStats] = useState<GitHubStats>({
|
||||
stars: 0,
|
||||
forks: 0,
|
||||
createdAt: '',
|
||||
daysOld: 0,
|
||||
isLoading: true,
|
||||
error: null,
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
const fetchGitHubStats = async () => {
|
||||
try {
|
||||
const response = await fetch(`https://api.github.com/repos/${owner}/${repo}`)
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch GitHub stats')
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
// Calculate days since creation
|
||||
const createdDate = new Date(data.created_at)
|
||||
const now = new Date()
|
||||
const diffTime = Math.abs(now.getTime() - createdDate.getTime())
|
||||
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24))
|
||||
|
||||
setStats({
|
||||
stars: data.stargazers_count,
|
||||
forks: data.forks_count,
|
||||
createdAt: data.created_at,
|
||||
daysOld: diffDays,
|
||||
isLoading: false,
|
||||
error: null,
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Error fetching GitHub stats:', error)
|
||||
setStats(prev => ({
|
||||
...prev,
|
||||
isLoading: false,
|
||||
error: error instanceof Error ? error.message : 'Unknown error',
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
fetchGitHubStats()
|
||||
}, [owner, repo])
|
||||
|
||||
return stats
|
||||
}
|
||||
Reference in New Issue
Block a user