Skip to content

Commit eb7521e

Browse files
committed
feat: add GitHub and LeetCode API integration endpoints and base client styles
1 parent 2c6e262 commit eb7521e

8 files changed

Lines changed: 1190 additions & 114 deletions

File tree

client/src/index.css

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
--color-dark-100: var(--theme-color-dark-100);
2626
--color-dark-200: var(--theme-color-dark-200);
2727
--color-dark-300: var(--theme-color-dark-300);
28+
--color-dark-350: var(--theme-color-dark-350);
2829
--color-dark-400: var(--theme-color-dark-400);
2930
--color-dark-500: var(--theme-color-dark-500);
3031
--color-dark-600: var(--theme-color-dark-600);
@@ -61,6 +62,7 @@
6162
--theme-color-dark-100: #ede9fe;
6263
--theme-color-dark-200: #ddd6fe;
6364
--theme-color-dark-300: #cbd5e1;
65+
--theme-color-dark-350: #64748b;
6466
--theme-color-dark-400: #94a3b8;
6567
--theme-color-dark-500: #6b7280;
6668
--theme-color-dark-600: #4b5563;
@@ -97,6 +99,7 @@ html.dark {
9799
--theme-color-dark-100: #e6edf3;
98100
--theme-color-dark-200: #8b949e;
99101
--theme-color-dark-300: #e6edf3;
102+
--theme-color-dark-350: #d1d5db;
100103
--theme-color-dark-400: #8b949e;
101104
--theme-color-dark-500: #8b949e;
102105
--theme-color-dark-600: #e6edf3;

client/src/pages/Profile.jsx

Lines changed: 835 additions & 109 deletions
Large diffs are not rendered by default.

client/src/services/api.js

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,10 @@ api.interceptors.response.use(
3434
// Handle concurrent session logout
3535
if (error.response?.status === 401 && error.response?.data?.code === 'SESSION_EXPIRED') {
3636
localStorage.removeItem('accessToken');
37-
window.location.href = '/login?error=session_expired';
37+
localStorage.removeItem('user');
38+
if (!window.location.pathname.startsWith('/p/')) {
39+
window.location.href = '/login?error=session_expired';
40+
}
3841
return Promise.reject(error);
3942
}
4043

@@ -62,9 +65,12 @@ api.interceptors.response.use(
6265
originalRequest.headers.Authorization = `Bearer ${accessToken}`;
6366
return api(originalRequest);
6467
} catch (refreshError) {
65-
// Refresh failed, clear tokens and redirect to login
68+
// Refresh failed, clear tokens and redirect to login if not viewing public profile
6669
localStorage.removeItem('accessToken');
67-
window.location.href = '/login';
70+
localStorage.removeItem('user');
71+
if (!window.location.pathname.startsWith('/p/')) {
72+
window.location.href = '/login';
73+
}
6874
return Promise.reject(refreshError);
6975
}
7076
}
@@ -99,6 +105,8 @@ export const projectsAPI = {
99105
getUserProjects: (userId) => api.get(`/projects/user/${userId}`),
100106
importGithub: (githubLink) => api.post('/projects/import-github', { githubLink }),
101107
deleteUserProject: (id) => api.delete(`/projects/user/${id}`),
108+
createUserProject: (data) => api.post('/projects/user', data),
109+
updateUserProject: (id, data) => api.put(`/projects/user/${id}`, data),
102110
};
103111

104112
// Certificates API
@@ -191,5 +199,11 @@ export const paymentAPI = {
191199
verifyPayment: (paymentData) => api.post('/payment/verify', paymentData),
192200
};
193201

202+
// Integrations API
203+
export const integrationsAPI = {
204+
getGithub: (username) => api.get(`/integrations/github/${username}`),
205+
getLeetcode: (username) => api.get(`/integrations/leetcode/${username}`),
206+
};
207+
194208
export default api;
195209

Lines changed: 224 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,224 @@
1+
// Using native Node global fetch
2+
3+
// @desc Get user's GitHub integration profile
4+
// @route GET /api/integrations/github/:username
5+
// @access Public
6+
const getGithubProfile = async (req, res) => {
7+
try {
8+
const { username } = req.params;
9+
if (!username) {
10+
return res.status(400).json({
11+
success: false,
12+
message: 'GitHub username is required'
13+
});
14+
}
15+
16+
// Fetch GitHub User Profile
17+
const profileRes = await fetch(`https://api.github.com/users/${username}`, {
18+
headers: { 'User-Agent': 'CodeForge-App' }
19+
});
20+
21+
if (!profileRes.ok) {
22+
return res.status(404).json({
23+
success: false,
24+
message: `GitHub user ${username} not found`
25+
});
26+
}
27+
28+
const profile = await profileRes.json();
29+
30+
// Fetch User Repositories
31+
const reposRes = await fetch(`https://api.github.com/users/${username}/repos?per_page=100&sort=updated`, {
32+
headers: { 'User-Agent': 'CodeForge-App' }
33+
});
34+
35+
let repos = [];
36+
if (reposRes.ok) {
37+
repos = await reposRes.json();
38+
}
39+
40+
const totalStars = repos.reduce((acc, repo) => acc + (repo.stargazers_count || 0), 0);
41+
const totalForks = repos.reduce((acc, repo) => acc + (repo.forks_count || 0), 0);
42+
43+
// Language breakdown
44+
const languagesMap = {};
45+
repos.forEach(repo => {
46+
if (repo.language) {
47+
languagesMap[repo.language] = (languagesMap[repo.language] || 0) + 1;
48+
}
49+
});
50+
51+
// Top 5 starred repositories
52+
const topRepos = repos
53+
.sort((a, b) => (b.stargazers_count || 0) - (a.stargazers_count || 0))
54+
.slice(0, 5)
55+
.map(r => ({
56+
name: r.name,
57+
description: r.description || '',
58+
stars: r.stargazers_count,
59+
forks: r.forks_count,
60+
url: r.html_url,
61+
language: r.language || ''
62+
}));
63+
64+
// Fetch contribution calendar HTML to extract contributions
65+
let contributions = [];
66+
try {
67+
const contribRes = await fetch(`https://github.com/users/${username}/contributions`, {
68+
headers: { 'User-Agent': 'CodeForge-App' }
69+
});
70+
if (contribRes.ok) {
71+
const html = await contribRes.text();
72+
// Match data-date="..." and data-level="..." attributes
73+
const regex = /data-date="(\d{4}-\d{2}-\d{2})"[^>]*data-level="(\d+)"/g;
74+
let match;
75+
while ((match = regex.exec(html)) !== null) {
76+
contributions.push({
77+
date: match[1],
78+
level: parseInt(match[2], 10)
79+
});
80+
}
81+
}
82+
} catch (err) {
83+
console.error('Failed to parse GitHub contributions HTML:', err);
84+
}
85+
86+
res.json({
87+
success: true,
88+
data: {
89+
avatar: profile.avatar_url,
90+
name: profile.name || profile.login,
91+
login: profile.login,
92+
bio: profile.bio || '',
93+
publicRepos: profile.public_repos,
94+
followers: profile.followers,
95+
following: profile.following,
96+
totalStars,
97+
totalForks,
98+
languages: languagesMap,
99+
topRepos,
100+
contributions
101+
}
102+
});
103+
} catch (error) {
104+
console.error('GitHub integration error:', error);
105+
res.status(500).json({
106+
success: false,
107+
message: 'Failed to fetch GitHub profile data',
108+
error: error.message
109+
});
110+
}
111+
};
112+
113+
// @desc Get user's LeetCode integration profile
114+
// @route GET /api/integrations/leetcode/:username
115+
// @access Public
116+
const getLeetcodeProfile = async (req, res) => {
117+
try {
118+
const { username } = req.params;
119+
if (!username) {
120+
return res.status(400).json({
121+
success: false,
122+
message: 'LeetCode username is required'
123+
});
124+
}
125+
126+
const query = `
127+
query leetcodeProfile($username: String!) {
128+
allQuestionsCount {
129+
difficulty
130+
count
131+
}
132+
matchedUser(username: $username) {
133+
username
134+
profile {
135+
realName
136+
ranking
137+
userAvatar
138+
reputation
139+
}
140+
submitStats {
141+
acSubmissionNum {
142+
difficulty
143+
count
144+
submissions
145+
}
146+
}
147+
}
148+
recentSubmissionList(username: $username, limit: 15) {
149+
title
150+
titleSlug
151+
statusDisplay
152+
lang
153+
timestamp
154+
}
155+
}
156+
`;
157+
158+
const response = await fetch('https://leetcode.com/graphql', {
159+
method: 'POST',
160+
headers: {
161+
'Content-Type': 'application/json',
162+
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
163+
},
164+
body: JSON.stringify({
165+
query,
166+
variables: { username }
167+
})
168+
});
169+
170+
if (!response.ok) {
171+
return res.status(response.status).json({
172+
success: false,
173+
message: `LeetCode API responded with status ${response.status}`
174+
});
175+
}
176+
177+
const result = await response.json();
178+
if (result.errors) {
179+
return res.status(400).json({
180+
success: false,
181+
message: result.errors[0]?.message || 'Error from LeetCode API'
182+
});
183+
}
184+
185+
const data = result.data;
186+
if (!data.matchedUser) {
187+
return res.status(404).json({
188+
success: false,
189+
message: `LeetCode user ${username} not found`
190+
});
191+
}
192+
193+
const allQuestions = data.allQuestionsCount || [];
194+
const matchedUser = data.matchedUser;
195+
const acSubmissions = matchedUser.submitStats?.acSubmissionNum || [];
196+
const submissions = data.recentSubmissionList || [];
197+
198+
res.json({
199+
success: true,
200+
data: {
201+
username: matchedUser.username,
202+
realName: matchedUser.profile?.realName || '',
203+
ranking: matchedUser.profile?.ranking || 0,
204+
avatar: matchedUser.profile?.userAvatar || '',
205+
reputation: matchedUser.profile?.reputation || 0,
206+
allQuestionsCount: allQuestions,
207+
acSubmissionNum: acSubmissions,
208+
submissions
209+
}
210+
});
211+
} catch (error) {
212+
console.error('LeetCode integration error:', error);
213+
res.status(500).json({
214+
success: false,
215+
message: 'Failed to fetch LeetCode profile data',
216+
error: error.message
217+
});
218+
}
219+
};
220+
221+
module.exports = {
222+
getGithubProfile,
223+
getLeetcodeProfile
224+
};

0 commit comments

Comments
 (0)