Skip to content

Commit 3820dcd

Browse files
committed
Add Github Star Button
1 parent 2c3c5b7 commit 3820dcd

5 files changed

Lines changed: 338 additions & 0 deletions

File tree

docusaurus.config.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,10 @@ const config: Config = {
5151
plugins: [
5252
],
5353

54+
// 客户端模块 - 添加GitHub点赞按钮
55+
clientModules: [
56+
require.resolve('./src/clientModules/GitHubStarButtonInjector.js'),
57+
],
5458

5559
// 导航栏和页脚设置
5660
themeConfig: {
@@ -79,6 +83,12 @@ const config: Config = {
7983
position: 'left',
8084
to: '/api-viewer',
8185
},
86+
// GitHub点赞按钮
87+
{
88+
type: 'html',
89+
position: 'right',
90+
value: '<div id="github-star-button"></div>',
91+
},
8292
// 多语言切换下拉菜单
8393
{
8494
type: 'localeDropdown',

i18n/en/code.json

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -487,5 +487,17 @@
487487
"project.api.loadingError": {
488488
"message": "Failed to load API documentation. Please refresh the page and try again.",
489489
"description": "API documentation loading error message"
490+
},
491+
"project.GitHub.Stared": {
492+
"message": "Stared",
493+
"description": "Github Stared"
494+
},
495+
"project.GitHub.Star": {
496+
"message": "Star",
497+
"description": "Github Star"
498+
},
499+
"project.GitHub.Stared.Failed": {
500+
"message": "Failed to star, please refresh and retry.",
501+
"description": "Github Star Failed"
490502
}
491503
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import ExecutionEnvironment from '@docusaurus/ExecutionEnvironment';
2+
import React from 'react';
3+
import ReactDOM from 'react-dom';
4+
import GitHubStarButton from '../components/GitHubStarButton';
5+
6+
// 仅在浏览器环境执行
7+
if (ExecutionEnvironment.canUseDOM) {
8+
const initGitHubStarButton = () => {
9+
const buttonContainer = document.getElementById('github-star-button');
10+
if (buttonContainer && !buttonContainer.hasChildNodes()) {
11+
ReactDOM.render(React.createElement(GitHubStarButton), buttonContainer);
12+
}
13+
};
14+
15+
// 页面加载后初始化
16+
window.addEventListener('load', initGitHubStarButton);
17+
18+
// 对于SPA导航,在路由变化后重新初始化
19+
document.addEventListener('docusaurus.routeDidUpdate', initGitHubStarButton);
20+
}
21+
22+
// 导出模块
23+
export default {
24+
onRouteDidUpdate() {
25+
if (ExecutionEnvironment.canUseDOM) {
26+
setTimeout(() => {
27+
const buttonContainer = document.getElementById('github-star-button');
28+
if (buttonContainer && !buttonContainer.hasChildNodes()) {
29+
ReactDOM.render(React.createElement(GitHubStarButton), buttonContainer);
30+
}
31+
}, 200);
32+
}
33+
},
34+
};
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
.container {
2+
display: flex;
3+
align-items: center;
4+
gap: 8px;
5+
margin: 0 10px;
6+
}
7+
8+
.error {
9+
color: #e53e3e;
10+
font-size: 12px;
11+
margin-bottom: 4px;
12+
}
13+
14+
.starButton {
15+
display: flex;
16+
align-items: center;
17+
justify-content: center;
18+
gap: 6px;
19+
background-color: #f3f4f6;
20+
border: 1px solid #d1d5db;
21+
border-radius: 6px;
22+
padding: 6px 12px;
23+
font-size: 14px;
24+
font-weight: 500;
25+
cursor: pointer;
26+
transition: all 0.2s ease;
27+
color: #24292e;
28+
}
29+
30+
.starButton:hover {
31+
background-color: #e5e7eb;
32+
}
33+
34+
.starButton:disabled {
35+
opacity: 0.6;
36+
cursor: not-allowed;
37+
}
38+
39+
.starred {
40+
background-color: #fef3c7;
41+
border-color: #fbbf24;
42+
color: #92400e;
43+
}
44+
45+
.starred:hover {
46+
background-color: #fde68a;
47+
}
48+
49+
.icon {
50+
fill: currentColor;
51+
}
52+
53+
.logoutButton {
54+
background: none;
55+
border: none;
56+
color: #6b7280;
57+
font-size: 12px;
58+
cursor: pointer;
59+
padding: 0;
60+
text-decoration: underline;
61+
}
62+
63+
.logoutButton:hover {
64+
color: #1f2937;
65+
}
66+
67+
/* 为小屏幕设备调整样式 */
68+
@media (max-width: 768px) {
69+
.container {
70+
margin: 0 5px;
71+
}
72+
73+
.starButton {
74+
padding: 4px 8px;
75+
font-size: 12px;
76+
}
77+
}
Lines changed: 205 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,205 @@
1+
import React, { useState, useEffect } from 'react';
2+
import styles from './GitHubStarButton.module.css';
3+
import { translate } from '@docusaurus/Translate';
4+
5+
// 配置信息
6+
const GITHUB_CLIENT_ID = 'Ov23libkfFAeZk8u5wFL'; // 您的GitHub OAuth应用Client ID
7+
const TARGET_REPO = 'LianjiaTech/bella-openapi'; // 您的目标仓库
8+
const GATEKEEPER_URL = 'https://api.bella.top/gatekeeper'; // http://gatekeeper.yourdomain.com
9+
interface TokenResponse {
10+
token: string;
11+
}
12+
13+
const GitHubStarButton: React.FC = () => {
14+
const [isAuthenticated, setIsAuthenticated] = useState<boolean>(false);
15+
const [isStarred, setIsStarred] = useState<boolean>(false);
16+
const [isLoading, setIsLoading] = useState<boolean>(false);
17+
const [error, setError] = useState<string | null>(null);
18+
19+
const errorStr = translate({
20+
id: 'project.GitHub.Stared.Failed',
21+
message: '点赞操作失败,请刷新后重试',
22+
description: 'Github Star Failed'
23+
});
24+
25+
// 当组件加载时检查认证状态和是否已点赞
26+
useEffect(() => {
27+
// 检查是否已登录(本地存储的token)
28+
const token = localStorage.getItem('github_token');
29+
if (token) {
30+
setIsAuthenticated(true);
31+
// 检查用户是否已经为仓库点赞
32+
checkIfStarred(token);
33+
}
34+
35+
// 检查URL中是否有授权码
36+
if (typeof window !== 'undefined') {
37+
const urlParams = new URLSearchParams(window.location.search);
38+
const code = urlParams.get('code');
39+
40+
if (code) {
41+
// 有授权码,通过Gatekeeper交换获取token
42+
exchangeCodeForToken(code);
43+
}
44+
}
45+
}, []);
46+
47+
// 检查用户是否已经为仓库点赞
48+
const checkIfStarred = async (token: string): Promise<void> => {
49+
try {
50+
const response = await fetch(`https://api.github.com/user/starred/${TARGET_REPO}`, {
51+
headers: {
52+
Authorization: `token ${token}`,
53+
Accept: 'application/vnd.github.v3+json',
54+
},
55+
});
56+
57+
// 204表示找到了,404表示没有找到(未点赞)
58+
setIsStarred(response.status === 204);
59+
} catch (err) {
60+
console.error('检查星标状态失败:', err);
61+
setIsStarred(false);
62+
}
63+
};
64+
65+
// 使用授权码通过Gatekeeper交换token
66+
const exchangeCodeForToken = async (code: string): Promise<void> => {
67+
setIsLoading(true);
68+
setError(null);
69+
70+
try {
71+
// 使用Gatekeeper交换token
72+
const response = await fetch(`${GATEKEEPER_URL}/authenticate/${code}`, {
73+
method: 'GET',
74+
headers: {
75+
Accept: 'application/json'
76+
}
77+
});
78+
79+
if (response.ok) {
80+
const data = await response.json() as TokenResponse;
81+
const token = data.token; // Gatekeeper返回的格式
82+
83+
if (token) {
84+
// 存储token到本地
85+
localStorage.setItem('github_token', token);
86+
setIsAuthenticated(true);
87+
88+
// 检查是否已点赞
89+
await checkIfStarred(token);
90+
91+
// 清除URL中的code参数
92+
if (typeof window !== 'undefined' && window.history.replaceState) {
93+
const newUrl = window.location.pathname +
94+
(window.location.search ? window.location.search.replace(/[?&]code=[^&]+/, '') : '');
95+
window.history.replaceState({}, document.title, newUrl);
96+
}
97+
98+
// 自动执行点赞操作
99+
await performStar(token);
100+
} else {
101+
throw new Error('响应中没有token');
102+
}
103+
} else {
104+
throw new Error('获取token失败');
105+
}
106+
} catch (err) {
107+
setError(errorStr);
108+
console.error('Token交换失败:', err);
109+
} finally {
110+
setIsLoading(false);
111+
}
112+
};
113+
114+
// 执行点赞操作
115+
const performStar = async (token: string): Promise<void> => {
116+
// 如果已经点赞了,什么都不做
117+
if (isStarred) {
118+
return;
119+
}
120+
121+
try {
122+
const response = await fetch(`https://api.github.com/user/starred/${TARGET_REPO}`, {
123+
method: 'PUT',
124+
headers: {
125+
Authorization: `token ${token}`,
126+
Accept: 'application/vnd.github.v3+json',
127+
'Content-Length': '0',
128+
},
129+
});
130+
131+
if (response.status === 204) {
132+
setIsStarred(true);
133+
} else {
134+
throw new Error('点赞操作失败');
135+
}
136+
} catch (err) {
137+
setError(errorStr);
138+
console.error('Star操作失败:', err);
139+
}
140+
};
141+
142+
// 处理登录
143+
const handleLogin = (): void => {
144+
const redirectUri = window.location.href.split('?')[0]; // 移除现有的查询参数
145+
const authUrl = `https://github.com/login/oauth/authorize?client_id=${GITHUB_CLIENT_ID}&redirect_uri=${encodeURIComponent(redirectUri)}&scope=public_repo`;
146+
147+
// 跳转到GitHub授权页面
148+
window.location.href = authUrl;
149+
};
150+
151+
// 处理点赞操作
152+
const handleStar = async (): Promise<void> => {
153+
if (!isAuthenticated) {
154+
handleLogin();
155+
return;
156+
}
157+
158+
// 如果已经点赞了,什么都不做
159+
if (isStarred) {
160+
return;
161+
}
162+
163+
setIsLoading(true);
164+
const token = localStorage.getItem('github_token');
165+
166+
if (!token) {
167+
setError(errorStr);
168+
setIsLoading(false);
169+
return;
170+
}
171+
172+
try {
173+
await performStar(token);
174+
} finally {
175+
setIsLoading(false);
176+
}
177+
};
178+
179+
return (
180+
<div className={styles.container}>
181+
{error && <div className={styles.error}>{error}</div>}
182+
183+
<button
184+
onClick={handleStar}
185+
className={`${styles.starButton} ${isStarred ? styles.starred : ''}`}
186+
disabled={isLoading}
187+
>
188+
<svg className={styles.icon} viewBox="0 0 16 16" width="16" height="16" aria-hidden="true">
189+
<path fillRule="evenodd" d="M8 .25a.75.75 0 01.673.418l1.882 3.815 4.21.612a.75.75 0 01.416 1.279l-3.046 2.97.719 4.192a.75.75 0 01-1.088.791L8 12.347l-3.766 1.98a.75.75 0 01-1.088-.79l.72-4.194L.818 6.374a.75.75 0 01.416-1.28l4.21-.611L7.327.668A.75.75 0 018 .25zm0 2.445L6.615 5.5a.75.75 0 01-.564.41l-3.097.45 2.24 2.184a.75.75 0 01.216.664l-.528 3.084 2.769-1.456a.75.75 0 01.698 0l2.77 1.456-.53-3.084a.75.75 0 01.216-.664l2.24-2.183-3.096-.45a.75.75 0 01-.564-.41L8 2.694v.001z"></path>
190+
</svg>
191+
{isStarred ? translate({
192+
id: 'project.GitHub.Stared',
193+
message: '已点赞',
194+
description: 'Github Stared'
195+
}) : translate({
196+
id: 'project.GitHub.Star',
197+
message: '点赞',
198+
description: 'Github Star'
199+
})}
200+
</button>
201+
</div>
202+
);
203+
};
204+
205+
export default GitHubStarButton;

0 commit comments

Comments
 (0)