Skip to content

Commit 016b328

Browse files
committed
feat: redesign OpenSource page and optimize scroll/animation performance
1 parent 08c052a commit 016b328

8 files changed

Lines changed: 750 additions & 63 deletions

File tree

Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
import { useEffect, useRef } from 'react';
2+
import styled from 'styled-components';
3+
import { useTranslation } from 'react-i18next';
4+
5+
const HeroCard = styled.div`
6+
display: flex;
7+
flex-direction: column;
8+
gap: 40px;
9+
margin-bottom: 64px;
10+
padding: 48px;
11+
border-radius: var(--radius-xl);
12+
border: 1px solid var(--border-color);
13+
background: var(--bg-elevated);
14+
box-shadow: var(--shadow-sm);
15+
position: relative;
16+
overflow: hidden;
17+
18+
&::before {
19+
content: '';
20+
position: absolute;
21+
top: 0;
22+
left: 0;
23+
right: 0;
24+
height: 4px;
25+
background: var(--gradient-accent);
26+
}
27+
28+
@media (min-width: 900px) {
29+
flex-direction: row;
30+
align-items: center;
31+
justify-content: space-between;
32+
}
33+
34+
@media (max-width: 768px) {
35+
padding: 32px 24px;
36+
gap: 32px;
37+
}
38+
`;
39+
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+
71+
const MetaGrid = styled.div`
72+
display: flex;
73+
gap: 32px;
74+
flex-wrap: wrap;
75+
76+
@media (max-width: 640px) {
77+
width: 100%;
78+
justify-content: space-between;
79+
gap: 24px;
80+
}
81+
`;
82+
83+
const MetaCard = styled.div`
84+
display: flex;
85+
flex-direction: column;
86+
gap: 4px;
87+
`;
88+
89+
const MetaLabel = styled.div`
90+
color: var(--text-tertiary);
91+
font-size: 0.85rem;
92+
font-weight: 500;
93+
text-transform: uppercase;
94+
letter-spacing: 0.05em;
95+
`;
96+
97+
const MetaValue = styled.div`
98+
font-family: var(--font-heading);
99+
font-size: 2.5rem;
100+
font-weight: 700;
101+
color: var(--text-primary);
102+
line-height: 1;
103+
`;
104+
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+
136+
interface HeroSectionProps {
137+
stats: {
138+
totalCount: number;
139+
activeCount: number;
140+
totalStars: number;
141+
};
142+
}
143+
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);
150+
151+
return (
152+
<HeroCard>
153+
<HeroText>
154+
<Eyebrow>{t('opensource.eyebrow')}</Eyebrow>
155+
<Title>{t('opensource.title')}</Title>
156+
<Description>{t('opensource.intro')}</Description>
157+
</HeroText>
158+
<MetaGrid>
159+
<MetaCard>
160+
<MetaLabel>{t('opensource.stats.total')}</MetaLabel>
161+
<MetaValue ref={totalRef}>0</MetaValue>
162+
</MetaCard>
163+
<MetaCard>
164+
<MetaLabel>{t('opensource.stats.active')}</MetaLabel>
165+
<MetaValue ref={activeRef}>0</MetaValue>
166+
</MetaCard>
167+
<MetaCard>
168+
<MetaLabel>{t('opensource.stats.stars')}</MetaLabel>
169+
<MetaValue ref={starsRef}>0</MetaValue>
170+
</MetaCard>
171+
</MetaGrid>
172+
</HeroCard>
173+
);
174+
};
175+
176+
export default HeroSection;
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import styled from 'styled-components';
2+
3+
export const RepoSection = styled.section`
4+
background: var(--bg-primary);
5+
padding: 80px 24px 100px;
6+
7+
@media (max-width: 768px) {
8+
padding: 56px 16px 80px;
9+
}
10+
`;
11+
12+
export const Content = styled.div`
13+
max-width: 1280px;
14+
margin: 0 auto;
15+
`;
16+
17+
export const Grid = styled.div`
18+
display: grid;
19+
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
20+
gap: 32px;
21+
22+
@media (max-width: 640px) {
23+
grid-template-columns: 1fr;
24+
gap: 24px;
25+
}
26+
`;
27+
28+
export const StatusContainer = styled.div`
29+
display: flex;
30+
justify-content: center;
31+
padding: 80px 0;
32+
color: var(--text-secondary);
33+
font-size: 1.1rem;
34+
`;
35+
36+
const languageColors: Record<string, string> = {
37+
TypeScript: '#3178c6',
38+
JavaScript: '#f7df1e',
39+
Vue: '#42b883',
40+
React: '#61dafb',
41+
Python: '#3776ab',
42+
Java: '#f89820',
43+
Kotlin: '#7f52ff',
44+
Go: '#00add8',
45+
Rust: '#dea584',
46+
Shell: '#89e051',
47+
HTML: '#e34f26',
48+
CSS: '#1572b6',
49+
SCSS: '#cc6699',
50+
};
51+
52+
export const getLanguageColor = (language: string | null): string => {
53+
if (!language) return 'var(--color-accent)';
54+
return languageColors[language] ?? 'var(--color-primary)';
55+
};

0 commit comments

Comments
 (0)