Skip to content

Commit 8642edf

Browse files
committed
feat: add footer statistics component with GitHub and app metrics display
1 parent dae3c92 commit 8642edf

18 files changed

Lines changed: 1795 additions & 216 deletions

‎app/[lang]/(home)/footer.tsx‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
import Link from "next/link";
22
import SectionWrap from "./section-wrap";
3+
import FooterStats from "@/components/footer-stats";
34

45
export default function HomeFooter() {
56
return (
67
<SectionWrap className="flex items-center justify-between">
7-
<div></div>
8+
<FooterStats />
89
<div className="flex flex-col gap-2 text-sm text-fd-muted-foreground">
910
<p>Released under the <Link href="https://github.com/codexu/note-gen/blob/dev/LICENSE">GPL-3.0 License</Link>.</p>
1011
<p>Copyright © 2024-present codexu</p>

‎app/[lang]/(home)/statistics.tsx‎

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
"use client"
2+
3+
import { useState, useEffect } from "react"
4+
import { AppStatisticsChart } from "@/components/app-statistics-chart"
5+
import { AppStatistics } from "@/lib/statistics-types"
6+
7+
export default function HomeStatistics() {
8+
const [statistics, setStatistics] = useState<AppStatistics | null>(null)
9+
const [loading, setLoading] = useState(true)
10+
const [error, setError] = useState<string | null>(null)
11+
12+
useEffect(() => {
13+
fetchStatistics()
14+
}, [])
15+
16+
const fetchStatistics = async () => {
17+
try {
18+
setLoading(true)
19+
setError(null)
20+
21+
// Fetch from internal API (no authentication required)
22+
const response = await fetch('/api/internal/statistics')
23+
24+
if (!response.ok) {
25+
throw new Error(`API request failed: ${response.status} ${response.statusText}`)
26+
}
27+
28+
const result = await response.json()
29+
30+
if (result.code !== 0) {
31+
throw new Error(`API error: ${result.msg}`)
32+
}
33+
34+
setStatistics(result.data)
35+
36+
} catch (err) {
37+
console.error('Error fetching statistics:', err);
38+
setError(err instanceof Error ? err.message : "获取统计数据失败")
39+
} finally {
40+
setLoading(false)
41+
}
42+
}
43+
44+
if (loading) {
45+
return (
46+
<section className="py-16">
47+
<div className="container mx-auto">
48+
<div className="flex items-center justify-center min-h-[200px]">
49+
<div className="text-center">
50+
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary mx-auto mb-4"></div>
51+
<p className="text-muted-foreground">加载统计数据中...</p>
52+
</div>
53+
</div>
54+
</div>
55+
</section>
56+
)
57+
}
58+
59+
if (error) {
60+
return (
61+
<section className="py-16">
62+
<div className="container mx-auto">
63+
<div className="flex items-center justify-center min-h-[200px]">
64+
<div className="text-center">
65+
<div className="text-destructive text-4xl mb-4">⚠️</div>
66+
<p className="text-destructive mb-4">{error}</p>
67+
<button
68+
onClick={fetchStatistics}
69+
className="px-4 py-2 bg-primary text-primary-foreground rounded-md hover:bg-primary/90"
70+
>
71+
重试
72+
</button>
73+
</div>
74+
</div>
75+
</div>
76+
</section>
77+
)
78+
}
79+
80+
if (!statistics) {
81+
return null
82+
}
83+
84+
return (
85+
<section className="py-16">
86+
<div className="container mx-auto space-y-8">
87+
<div className="text-center space-y-4">
88+
<h2 className="text-3xl font-bold">应用统计</h2>
89+
<p className="text-muted-foreground max-w-2xl mx-auto">
90+
查看应用的下载和启动统计数据,了解用户使用趋势
91+
</p>
92+
</div>
93+
94+
{/* Summary Cards */}
95+
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
96+
<div className="bg-card rounded-lg p-6 border">
97+
<div className="text-sm font-medium text-muted-foreground mb-2">昨日下载</div>
98+
<div className="text-2xl font-bold">{statistics.yesterdayDownloadCount.toLocaleString()}</div>
99+
</div>
100+
<div className="bg-card rounded-lg p-6 border">
101+
<div className="text-sm font-medium text-muted-foreground mb-2">总下载量</div>
102+
<div className="text-2xl font-bold">{statistics.totalDownloadCount.toLocaleString()}</div>
103+
{statistics.githubDownloads && (
104+
<div className="text-xs text-muted-foreground mt-1">
105+
包含 GitHub: {statistics.githubDownloads.toLocaleString()}
106+
</div>
107+
)}
108+
</div>
109+
<div className="bg-card rounded-lg p-6 border">
110+
<div className="text-sm font-medium text-muted-foreground mb-2">昨日启动</div>
111+
<div className="text-2xl font-bold">{statistics.yesterdayAppStartCount.toLocaleString()}</div>
112+
</div>
113+
<div className="bg-card rounded-lg p-6 border">
114+
<div className="text-sm font-medium text-muted-foreground mb-2">总启动量</div>
115+
<div className="text-2xl font-bold">{statistics.totalAppStartCount.toLocaleString()}</div>
116+
</div>
117+
</div>
118+
119+
{/* GitHub Stats */}
120+
{statistics.githubStars !== undefined && (
121+
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
122+
<div className="bg-card rounded-lg p-6 border">
123+
<div className="text-sm font-medium text-muted-foreground mb-2">GitHub Stars</div>
124+
<div className="text-2xl font-bold">{statistics.githubStars.toLocaleString()}</div>
125+
</div>
126+
<div className="bg-card rounded-lg p-6 border">
127+
<div className="text-sm font-medium text-muted-foreground mb-2">GitHub Forks</div>
128+
<div className="text-2xl font-bold">{statistics.githubForks?.toLocaleString() || 0}</div>
129+
</div>
130+
<div className="bg-card rounded-lg p-6 border">
131+
<div className="text-sm font-medium text-muted-foreground mb-2">GitHub 下载</div>
132+
<div className="text-2xl font-bold">{statistics.githubDownloads?.toLocaleString() || 0}</div>
133+
</div>
134+
</div>
135+
)}
136+
137+
{/* Chart */}
138+
<AppStatisticsChart
139+
downloadData={statistics.downloadCount7Day}
140+
startData={statistics.appStartCount7Day}
141+
/>
142+
</div>
143+
</section>
144+
)
145+
}

‎app/api/app/statistics/info/route.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ export async function GET(request: NextRequest) {
4343
const body = '';
4444

4545
// Validate signature
46-
const secretKey = process.env.SECRET_KEY;
46+
const secretKey = process.env.UPGRADELINK_SECRET_KEY;
4747
if (!secretKey) {
4848
const errorResponse: ErrorResponse = {
4949
code: 500001,
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
// GitHub API service for fetching repository statistics
2+
3+
interface GitHubRepoStats {
4+
stargazers_count: number;
5+
watchers_count: number;
6+
forks_count: number;
7+
open_issues_count: number;
8+
subscribers_count: number;
9+
}
10+
11+
interface GitHubReleaseAsset {
12+
name: string;
13+
download_count: number;
14+
}
15+
16+
interface GitHubRelease {
17+
tag_name: string;
18+
assets: GitHubReleaseAsset[];
19+
}
20+
21+
// GitHub API 缓存
22+
let githubCache: {
23+
data: {
24+
totalDownloads: number;
25+
stars: number;
26+
watchers: number;
27+
forks: number;
28+
} | null;
29+
timestamp: number;
30+
} = {
31+
data: null,
32+
timestamp: 0
33+
};
34+
35+
const GITHUB_CACHE_DURATION = 60 * 60 * 1000; // 1 hour in milliseconds
36+
37+
export async function getGitHubRepoStats(owner: string, repo: string): Promise<{
38+
totalDownloads: number;
39+
stars: number;
40+
watchers: number;
41+
forks: number;
42+
}> {
43+
const now = Date.now();
44+
45+
// 检查缓存是否有效
46+
if (githubCache.data && (now - githubCache.timestamp) < GITHUB_CACHE_DURATION) {
47+
console.log('Using cached GitHub stats');
48+
return githubCache.data;
49+
}
50+
51+
const token = process.env.GITHUB_TOKEN;
52+
53+
if (!token) {
54+
console.warn('GITHUB_TOKEN not found, skipping GitHub stats');
55+
return {
56+
totalDownloads: 0,
57+
stars: 0,
58+
watchers: 0,
59+
forks: 0,
60+
};
61+
}
62+
63+
try {
64+
// Get repository info
65+
const repoResponse = await fetch(`https://api.github.com/repos/${owner}/${repo}`, {
66+
headers: {
67+
'Authorization': `token ${token}`,
68+
'Accept': 'application/vnd.github.v3+json',
69+
'User-Agent': 'note-gen-docs-stats',
70+
},
71+
});
72+
73+
if (!repoResponse.ok) {
74+
throw new Error(`GitHub API error: ${repoResponse.status} ${repoResponse.statusText}`);
75+
}
76+
77+
const repoData: GitHubRepoStats = await repoResponse.json();
78+
79+
// Get all releases to calculate total downloads
80+
const releasesResponse = await fetch(`https://api.github.com/repos/${owner}/${repo}/releases?per_page=100`, {
81+
headers: {
82+
'Authorization': `token ${token}`,
83+
'Accept': 'application/vnd.github.v3+json',
84+
'User-Agent': 'note-gen-docs-stats',
85+
},
86+
});
87+
88+
if (!releasesResponse.ok) {
89+
throw new Error(`GitHub releases API error: ${releasesResponse.status} ${releasesResponse.statusText}`);
90+
}
91+
92+
const releases: GitHubRelease[] = await releasesResponse.json();
93+
94+
// Calculate total downloads from all release assets
95+
let totalDownloads = 0;
96+
releases.forEach(release => {
97+
release.assets.forEach(asset => {
98+
totalDownloads += asset.download_count;
99+
});
100+
});
101+
102+
const stats = {
103+
totalDownloads,
104+
stars: repoData.stargazers_count,
105+
watchers: repoData.watchers_count,
106+
forks: repoData.forks_count,
107+
};
108+
109+
// 更新缓存
110+
githubCache.data = stats;
111+
githubCache.timestamp = now;
112+
113+
return stats;
114+
115+
} catch (error) {
116+
console.error('Error fetching GitHub stats:', error);
117+
118+
// 如果API调用失败但有缓存数据,返回缓存数据
119+
if (githubCache.data) {
120+
console.log('GitHub API failed, returning stale cache data');
121+
return githubCache.data;
122+
}
123+
124+
return {
125+
totalDownloads: 0,
126+
stars: 0,
127+
watchers: 0,
128+
forks: 0,
129+
};
130+
}
131+
}

0 commit comments

Comments
 (0)