Skip to content

Commit dc20707

Browse files
committed
added gitrank
1 parent fea2366 commit dc20707

5 files changed

Lines changed: 959 additions & 193 deletions

File tree

firestore.rules

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,18 @@ service cloud.firestore {
5858
(resource.data.onboardingStatus == "incomplete" && request.resource.data.onboardingStatus == "complete") ||
5959
// Or points are unchanged or incremented by small, legitimate amounts (like a streak login +2, codingverse easy +5)
6060
(request.resource.data.points.totalPoints == resource.data.points.totalPoints) ||
61-
(request.resource.data.points.totalPoints > resource.data.points.totalPoints && request.resource.data.points.totalPoints - resource.data.points.totalPoints <= 100)
61+
(request.resource.data.points.totalPoints > resource.data.points.totalPoints && request.resource.data.points.totalPoints - resource.data.points.totalPoints <= 100) ||
62+
// Or they are updating their GitHub stats, and the GitRank points matches the formula, and other points are unchanged:
63+
(
64+
request.resource.data.points.gitRankPoints == (
65+
request.resource.data.githubStats.commits * 2 +
66+
request.resource.data.githubStats.prs * 5 +
67+
request.resource.data.githubStats.reviews * 10
68+
) &&
69+
request.resource.data.points.codingVersePoints == resource.data.points.codingVersePoints &&
70+
request.resource.data.points.streakPoints == resource.data.points.streakPoints &&
71+
request.resource.data.points.referralPoints == resource.data.points.referralPoints
72+
)
6273
);
6374

6475
// RULE 3: Referrer update - Allow another user to atomically increment a referrer's referralPoints by exactly 100 points

src/components/dashboard/RankPreview.jsx

Lines changed: 88 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,70 @@
1-
import React from "react";
1+
import React, { useState, useEffect } from "react";
22
import { Link } from "react-router-dom";
33
import { Trophy, ChevronRight } from "lucide-react";
4-
import { leaderboardData } from "../../data/leaderboard";
4+
import { collection, query, where, onSnapshot } from "firebase/firestore";
5+
import { db } from "../../lib/firebase";
6+
import { useAuth } from "../../context/AuthContext";
57
import Card from "../ui/Card";
68

79
export const RankPreview = () => {
8-
// Grab top 3 users
9-
const topThree = leaderboardData.slice(0, 3);
10+
const { user } = useAuth();
11+
const [topThree, setTopThree] = useState([]);
12+
const [loading, setLoading] = useState(true);
1013

1114
const medalColors = {
1215
1: "bg-amber-500 text-white shadow-[0_0_15px_rgba(245,158,11,0.4)]",
1316
2: "bg-slate-300 text-slate-900 border border-slate-400/30",
1417
3: "bg-orange-600 text-white"
1518
};
1619

20+
useEffect(() => {
21+
if (!user) {
22+
const timer = setTimeout(() => {
23+
setLoading(false);
24+
}, 0);
25+
return () => clearTimeout(timer);
26+
}
27+
28+
const q = query(
29+
collection(db, "users"),
30+
where("onboardingStatus", "==", "complete")
31+
);
32+
33+
const unsubscribe = onSnapshot(
34+
q,
35+
(snapshot) => {
36+
const users = [];
37+
snapshot.forEach((doc) => {
38+
users.push(doc.data());
39+
});
40+
41+
// Sort by totalPoints descending
42+
users.sort((a, b) => (b.points?.totalPoints || 0) - (a.points?.totalPoints || 0));
43+
44+
// Slice top 3
45+
const top3 = users.slice(0, 3).map((u, i) => ({
46+
...u,
47+
rank: i + 1
48+
}));
49+
50+
setTopThree(top3);
51+
setLoading(false);
52+
},
53+
(error) => {
54+
console.error("RankPreview query failed:", error);
55+
setLoading(false);
56+
}
57+
);
58+
59+
return () => unsubscribe();
60+
}, [user]);
61+
1762
return (
1863
<Card className="flex flex-col h-full">
1964
{/* Header */}
2065
<div className="flex items-center justify-between pb-4 border-b border-slate-100 dark:border-slate-800">
2166
<div>
22-
<h3 className="font-extrabold text-lg text-slate-900 dark:text-white">Leaderboard Preview</h3>
67+
<h3 className="font-extrabold text-lg text-slate-900 dark:text-white my-0">Leaderboard Preview</h3>
2368
<p className="text-xs text-slate-400 dark:text-slate-500">Top performers this week</p>
2469
</div>
2570
<Link
@@ -32,43 +77,51 @@ export const RankPreview = () => {
3277

3378
{/* Ranks Cards List */}
3479
<div className="flex-1 mt-6 space-y-4">
35-
{topThree.map((user) => (
36-
<div
37-
key={user.rank}
38-
className="flex items-center justify-between p-3 rounded-xl border border-slate-200/40 dark:border-slate-800/40 bg-slate-50/50 dark:bg-slate-950/30 hover:bg-slate-50 dark:hover:bg-slate-800/50 transition-colors"
39-
>
40-
<div className="flex items-center gap-3">
41-
{/* Rank Medal */}
42-
<div className={`w-6 h-6 rounded-full flex items-center justify-center text-xs font-bold ${medalColors[user.rank] || "bg-slate-200"}`}>
43-
{user.rank === 1 ? <Trophy className="w-3.5 h-3.5" /> : user.rank}
44-
</div>
80+
{loading ? (
81+
<div className="h-full flex items-center justify-center py-10">
82+
<div className="w-5 h-5 border-2 border-violet-500 border-t-transparent rounded-full animate-spin" />
83+
</div>
84+
) : topThree.length > 0 ? (
85+
topThree.map((user) => (
86+
<div
87+
key={user.uid}
88+
className="flex items-center justify-between p-3 rounded-xl border border-slate-200/40 dark:border-slate-800/40 bg-slate-50/50 dark:bg-slate-950/30 hover:bg-slate-50 dark:hover:bg-slate-800/50 transition-colors"
89+
>
90+
<div className="flex items-center gap-3">
91+
{/* Rank Medal */}
92+
<div className={`w-6 h-6 rounded-full flex items-center justify-center text-xs font-bold ${medalColors[user.rank] || "bg-slate-200"}`}>
93+
{user.rank === 1 ? <Trophy className="w-3.5 h-3.5" /> : user.rank}
94+
</div>
95+
96+
{/* User Info */}
97+
<div className="w-9 h-9 rounded-lg overflow-hidden flex-shrink-0">
98+
<img src={user.avatar} alt={user.name} className="w-full h-full object-cover" />
99+
</div>
45100

46-
{/* User Info */}
47-
<div className="w-9 h-9 rounded-lg overflow-hidden flex-shrink-0">
48-
<img src={user.avatar} alt={user.name} className="w-full h-full object-cover" />
101+
<div>
102+
<span className="text-sm font-extrabold text-slate-900 dark:text-slate-200 block leading-tight">
103+
{user.name}
104+
</span>
105+
<span className="text-[10px] font-semibold text-slate-400 dark:text-slate-500 block">
106+
@{user.githubUsername}{user.githubStats?.primaryLanguage || "JavaScript"}
107+
</span>
108+
</div>
49109
</div>
50110

51-
<div>
52-
<span className="text-sm font-extrabold text-slate-900 dark:text-slate-200 block leading-tight">
53-
{user.name}
111+
{/* Points / Streak */}
112+
<div className="text-right">
113+
<span className="block text-sm font-extrabold text-slate-900 dark:text-white leading-tight">
114+
{user.points?.totalPoints?.toLocaleString() || 0}
54115
</span>
55-
<span className="text-[10px] font-semibold text-slate-400 dark:text-slate-500 block">
56-
@{user.username}{user.language}
116+
<span className="inline-flex items-center gap-0.5 text-[10px] font-bold text-orange-500 dark:text-orange-400">
117+
🔥 {user.streak || 1}d streak
57118
</span>
58119
</div>
59120
</div>
60-
61-
{/* Points / Streak */}
62-
<div className="text-right">
63-
<span className="block text-sm font-extrabold text-slate-900 dark:text-white leading-tight">
64-
{user.points.toLocaleString()}
65-
</span>
66-
<span className="inline-flex items-center gap-0.5 text-[10px] font-bold text-orange-500 dark:text-orange-400">
67-
🔥 {user.streak}d streak
68-
</span>
69-
</div>
70-
</div>
71-
))}
121+
))
122+
) : (
123+
<p className="text-xs text-center text-slate-400 py-10">No ranked users yet</p>
124+
)}
72125
</div>
73126
</Card>
74127
);

src/context/AuthContext.jsx

Lines changed: 47 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -171,45 +171,84 @@ export const AuthProvider = ({ children }) => {
171171
const publicRepos = profileRes.data.public_repos || 0;
172172
const followers = profileRes.data.followers || 0;
173173

174-
// 2. Fetch repos to sum up stargazers
174+
// 2. Fetch repos to sum up stargazers and calculate primary language
175175
let stars = 0;
176+
let primaryLanguage = "JavaScript";
176177
try {
177178
const reposRes = await axios.get(`https://api.github.com/users/${username}/repos?per_page=100&type=owner`, { headers });
178179
stars = reposRes.data.reduce((sum, r) => sum + (r.stargazers_count || 0), 0);
180+
181+
// Count language frequency
182+
const langCounts = {};
183+
reposRes.data.forEach(r => {
184+
if (r.language) {
185+
langCounts[r.language] = (langCounts[r.language] || 0) + 1;
186+
}
187+
});
188+
const sortedLangs = Object.keys(langCounts).sort((a, b) => langCounts[b] - langCounts[a]);
189+
if (sortedLangs.length > 0) {
190+
primaryLanguage = sortedLangs[0];
191+
}
179192
} catch (err) {
180-
console.warn("Stars retrieval warning, defaulting stars to 0:", err);
193+
console.warn("Stars/Language retrieval warning, defaulting:", err);
181194
}
182195

183196
// 3. Fetch total commits using Search API (authenticated allows up to 30 requests/min securely)
184197
let commits = 0;
185198
try {
186199
const commitsRes = await axios.get(`https://api.github.com/search/commits?q=author:${username}`, { headers });
187200
commits = commitsRes.data.total_count || 0;
188-
} catch {
189-
// Fallback calculation in case of search commit API limits / private scope restrictions
201+
} catch (err) {
202+
console.warn("Commits retrieval warning, using fallback:", err);
190203
commits = publicRepos * 12;
191204
}
192205

193-
// Calculate initial GitRank points securely:
194-
// Commits -> +2, Repos -> +5, Stars -> +3, Followers -> +2
195-
const gitRankPoints = (commits * 2) + (publicRepos * 5) + (stars * 3) + (followers * 2);
206+
// 4. Fetch total pull requests
207+
let prs = 0;
208+
try {
209+
const prsRes = await axios.get(`https://api.github.com/search/issues?q=author:${username}+type:pr`, { headers });
210+
prs = prsRes.data.total_count || 0;
211+
} catch (err) {
212+
console.warn("PRs retrieval warning:", err);
213+
prs = Math.floor(publicRepos * 1.5);
214+
}
215+
216+
// 5. Fetch total reviews (PRs reviewed by user)
217+
let reviews = 0;
218+
try {
219+
const reviewsRes = await axios.get(`https://api.github.com/search/issues?q=reviewed-by:${username}`, { headers });
220+
reviews = reviewsRes.data.total_count || 0;
221+
} catch (err) {
222+
console.warn("Reviews retrieval warning:", err);
223+
reviews = Math.floor(prs * 0.2); // safe fallback
224+
}
225+
226+
// Calculate initial GitRank points securely based on real work only:
227+
// Commits -> +2, PRs -> +5, Reviews -> +10
228+
const gitRankPoints = (commits * 2) + (prs * 5) + (reviews * 10);
196229

197230
return {
198231
commits,
232+
prs,
233+
reviews,
199234
publicRepos,
200235
stars,
201236
followers,
237+
primaryLanguage,
202238
gitRankPoints
203239
};
204240
} catch (error) {
205241
console.error("Error executing GitHub stats fetcher snapshot:", error);
206242
// Clean fallback if API breaks entirely or token is invalid
207243
return {
208244
commits: 5,
245+
prs: 1,
246+
reviews: 0,
209247
publicRepos: 3,
210248
stars: 0,
211249
followers: 1,
212-
gitRankPoints: 25 // safe baseline
250+
primaryLanguage: "JavaScript",
251+
gitRankPoints: 15 // safe baseline
213252
};
214253
}
215254
};

0 commit comments

Comments
 (0)