1- import { useEffect , useRef } from 'react' ;
21import styled from 'styled-components' ;
32import { useTranslation } from 'react-i18next' ;
3+ import { DatabaseOutlined , FireOutlined , StarOutlined } from '@ant-design/icons' ;
44
55const HeroCard = styled . div `
66 display: flex;
7- flex-direction: column;
8- gap: 40px;
7+ justify-content: center;
98 margin-bottom: 64px;
109 padding: 48px;
1110 border-radius: var(--radius-xl);
1211 border: 1px solid var(--border-color);
13- background: var(--bg-elevated);
12+ background: linear-gradient(145deg, var(--bg-elevated), var(--bg-primary) );
1413 box-shadow: var(--shadow-sm);
1514 position: relative;
1615 overflow: hidden;
@@ -21,152 +20,135 @@ const HeroCard = styled.div`
2120 top: 0;
2221 left: 0;
2322 right: 0;
24- height: 4px ;
23+ height: 6px ;
2524 background: var(--gradient-accent);
25+ opacity: 0.8;
2626 }
2727
2828 @media (min-width: 900px) {
29- flex-direction: row;
3029 align-items: center;
31- justify-content: space-between;
3230 }
3331
3432 @media (max-width: 768px) {
3533 padding: 32px 24px;
36- gap: 32px;
3734 }
3835` ;
3936
40- const HeroText = styled . div `
41- display: flex;
42- flex-direction: column;
43- gap: 16px;
44- max-width: 600px;
45- ` ;
46-
47- const Eyebrow = styled . span `
48- font-size: 0.85rem;
49- font-weight: 600;
50- letter-spacing: 0.1em;
51- text-transform: uppercase;
52- color: var(--color-primary);
53- ` ;
54-
55- const Title = styled . h2 `
56- margin: 0;
57- font-family: var(--font-heading);
58- font-size: clamp(2.5rem, 4vw, 3.5rem);
59- font-weight: 700;
60- line-height: 1.1;
61- color: var(--text-primary);
62- ` ;
63-
64- const Description = styled . p `
65- margin: 0;
66- color: var(--text-secondary);
67- font-size: 1.05rem;
68- line-height: 1.7;
69- ` ;
70-
7137const MetaGrid = styled . div `
7238 display: flex;
73- gap: 32px;
39+ justify-content: center;
40+ gap: 48px;
7441 flex-wrap: wrap;
7542
7643 @media (max-width: 640px) {
7744 width: 100%;
78- justify-content: space-between;
45+ flex-direction: column;
46+ justify-content: center;
7947 gap: 24px;
8048 }
8149` ;
8250
8351const MetaCard = styled . div `
8452 display: flex;
8553 flex-direction: column;
86- gap: 4px;
54+ align-items: center;
55+ gap: 12px;
56+ padding: 24px 40px;
57+ border-radius: var(--radius-lg);
58+ background: var(--bg-secondary);
59+ border: 1px solid transparent;
60+ transition: all 0.3s ease;
61+
62+ &:hover {
63+ transform: translateY(-4px);
64+ box-shadow: var(--shadow-md);
65+ border-color: var(--border-color);
66+ background: var(--bg-elevated);
67+
68+ .anticon {
69+ color: var(--color-accent);
70+ transform: scale(1.1);
71+ }
72+ }
73+
74+ @media (max-width: 640px) {
75+ width: 100%;
76+ padding: 24px;
77+ }
8778` ;
8879
8980const MetaLabel = styled . div `
81+ display: flex;
82+ align-items: center;
83+ gap: 8px;
9084 color: var(--text-tertiary);
91- font-size: 0.85rem ;
92- font-weight: 500 ;
85+ font-size: 0.95rem ;
86+ font-weight: 600 ;
9387 text-transform: uppercase;
94- letter-spacing: 0.05em;
88+ letter-spacing: 0.1em;
89+
90+ .anticon {
91+ font-size: 1.1rem;
92+ transition: all 0.3s ease;
93+ }
9594` ;
9695
9796const MetaValue = styled . div `
9897 font-family: var(--font-heading);
99- font-size: 2.5rem ;
100- font-weight: 700 ;
98+ font-size: 3.2rem ;
99+ font-weight: 800 ;
101100 color: var(--text-primary);
102101 line-height: 1;
102+ text-shadow: 0 2px 10px rgba(0, 0, 0, 0.05);
103103` ;
104104
105- // Direct DOM mutation ticker, constant speed (linear)
106- const useNumberTicker = ( end : number , duration : number = 1000 ) => {
107- const nodeRef = useRef < HTMLDivElement > ( null ) ;
108-
109- useEffect ( ( ) => {
110- let startTime : number | null = null ;
111- let animationFrame : number ;
112-
113- const step = ( timestamp : number ) => {
114- if ( ! startTime ) startTime = timestamp ;
115- const progress = Math . min ( ( timestamp - startTime ) / duration , 1 ) ;
116-
117- // Linear progression (勻速)
118- const currentValue = Math . floor ( progress * end ) ;
119-
120- if ( nodeRef . current ) {
121- nodeRef . current . textContent = currentValue . toString ( ) ;
122- }
123-
124- if ( progress < 1 ) {
125- animationFrame = window . requestAnimationFrame ( step ) ;
126- }
127- } ;
128-
129- animationFrame = window . requestAnimationFrame ( step ) ;
130- return ( ) => window . cancelAnimationFrame ( animationFrame ) ;
131- } , [ end , duration ] ) ;
132-
133- return nodeRef ;
134- } ;
135-
136105interface HeroSectionProps {
106+ loading : boolean ;
137107 stats : {
138108 totalCount : number ;
139109 activeCount : number ;
140110 totalStars : number ;
141111 } ;
142112}
143113
144- const HeroSection = ( { stats } : HeroSectionProps ) => {
145- const { t } = useTranslation ( ) ;
146-
147- const totalRef = useNumberTicker ( stats . totalCount ) ;
148- const activeRef = useNumberTicker ( stats . activeCount ) ;
149- const starsRef = useNumberTicker ( stats . totalStars ) ;
114+ const getNumberLocale = ( language : string ) => {
115+ const normalizedLocale = language . replace ( '_' , '-' ) ;
116+
117+ return Intl . NumberFormat . supportedLocalesOf ( [ normalizedLocale ] ) [ 0 ] ?? 'en-US' ;
118+ } ;
119+
120+ const HeroSection = ( { loading, stats } : HeroSectionProps ) => {
121+ const { t, i18n } = useTranslation ( ) ;
122+ const numberLocale = getNumberLocale ( i18n . resolvedLanguage ?? i18n . language ) ;
123+
124+ const formatValue = ( value : number ) => {
125+ if ( loading ) {
126+ return '--' ;
127+ }
128+
129+ return value . toLocaleString ( numberLocale ) ;
130+ } ;
150131
151132 return (
152133 < HeroCard >
153- < HeroText >
154- < Eyebrow > { t ( 'opensource.eyebrow' ) } </ Eyebrow >
155- < Title > { t ( 'opensource.title' ) } </ Title >
156- < Description > { t ( 'opensource.intro' ) } </ Description >
157- </ HeroText >
158134 < MetaGrid >
159135 < MetaCard >
160- < MetaLabel > { t ( 'opensource.stats.total' ) } </ MetaLabel >
161- < MetaValue ref = { totalRef } > 0</ MetaValue >
136+ < MetaLabel >
137+ < DatabaseOutlined /> { t ( 'opensource.stats.total' ) }
138+ </ MetaLabel >
139+ < MetaValue > { formatValue ( stats . totalCount ) } </ MetaValue >
162140 </ MetaCard >
163141 < MetaCard >
164- < MetaLabel > { t ( 'opensource.stats.active' ) } </ MetaLabel >
165- < MetaValue ref = { activeRef } > 0</ MetaValue >
142+ < MetaLabel >
143+ < FireOutlined /> { t ( 'opensource.stats.active' ) }
144+ </ MetaLabel >
145+ < MetaValue > { formatValue ( stats . activeCount ) } </ MetaValue >
166146 </ MetaCard >
167147 < MetaCard >
168- < MetaLabel > { t ( 'opensource.stats.stars' ) } </ MetaLabel >
169- < MetaValue ref = { starsRef } > 0</ MetaValue >
148+ < MetaLabel >
149+ < StarOutlined /> { t ( 'opensource.stats.stars' ) }
150+ </ MetaLabel >
151+ < MetaValue > { formatValue ( stats . totalStars ) } </ MetaValue >
170152 </ MetaCard >
171153 </ MetaGrid >
172154 </ HeroCard >
0 commit comments