Skip to content

Commit fd0f365

Browse files
authored
Merge pull request #194 from oss-slu/feature/leaderboard-streak
Added streak-based leaderboard logic and UI updates
2 parents 584dbf4 + 71ee5e2 commit fd0f365

6 files changed

Lines changed: 217 additions & 57 deletions

File tree

Backend/streakCalculation.py

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
from datetime import datetime, timedelta
22

3-
ISSUE_THRESHOLD = 3
3+
ISSUE_THRESHOLD = 1
44

55
def get_week_start(date_obj):
66
"""
7-
Returns the Monday of the week for a given data
7+
Returns the Monday of the week for a given date
88
"""
99
return (date_obj - timedelta(days=date_obj.weekday())).date()
1010

@@ -63,15 +63,16 @@ def calculate_current_streak(closed_issue_dates, threshold=ISSUE_THRESHOLD, refe
6363
Returns:
6464
int: current streak count
6565
"""
66-
if reference_date is None:
67-
reference_date = datetime.now()
68-
6966
weekly_counts = group_issues_by_week(closed_issue_dates)
7067

7168
if not weekly_counts:
7269
return 0
7370

74-
current_week_start = get_week_start(reference_date)
71+
if reference_date is None:
72+
current_week_start = max(weekly_counts.keys())
73+
else:
74+
current_week_start = get_week_start(reference_date)
75+
7576
streak = 0
7677

7778
while weekly_counts.get(current_week_start, 0) >= threshold:

Frontend/src/components/TopContributorsRepos.jsx

Lines changed: 72 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -27,35 +27,84 @@ const TopContributorsRepos = () => {
2727
<h3>
2828
Top Contributors{" "}
2929
<span
30+
style={{
31+
position: "relative",
32+
display: "inline-block",
33+
marginLeft: "6px"
34+
}}
35+
>
36+
<span
3037
tabIndex="0"
3138
aria-label="Leaderboard streak info"
32-
title="A streak increases for every consecutive 7-day period where a user closes at least 3 issues. Streak resets if threshold is not met."
33-
style={{ cursor: "pointer", border: "1px solid black", borderRadius: "50%", padding: "2px 6px", fontSize: "12px" }}
39+
style={{
40+
cursor: "pointer",
41+
border: "1px solid black",
42+
borderRadius: "50%",
43+
padding: "2px 6px",
44+
fontSize: "12px",
45+
display: "inline-block"
46+
}}
47+
onMouseEnter={(e) => {
48+
const tooltip = e.currentTarget.nextElementSibling;
49+
if (tooltip) tooltip.style.visibility = "visible";
50+
if (tooltip) tooltip.style.opacity = "1";
51+
}}
52+
onMouseLeave={(e) => {
53+
const tooltip = e.currentTarget.nextElementSibling;
54+
if (tooltip) tooltip.style.visibility = "hidden";
55+
if (tooltip) tooltip.style.opacity = "0";
56+
}}
3457
>
3558
i
3659
</span>
37-
</h3>
38-
<ul>
39-
{topContributors.map((user) => (
40-
<li key={user.name}>
41-
{user.name} 🔥 {user.currentStreak} {user.currentStreak === 1 ? "week" : "weeks"} streak ({user.count})
42-
</li>
43-
))}
44-
</ul>
45-
</div>
46-
47-
{/* Render list of top repositories by activity count */}
48-
<div>
49-
<h3>Top Repositories</h3>
50-
<ul>
51-
{topRepos.map((repo) => (
52-
<li key={repo.name}>
53-
{repo.name} ({repo.count})
54-
</li>
55-
))}
56-
</ul>
57-
</div>
60+
61+
<span
62+
style={{
63+
visibility: "hidden",
64+
opacity: 0,
65+
transition: "opacity 0.2s ease",
66+
position: "absolute",
67+
top: "28px",
68+
left: "50%",
69+
transform: "translateX(-50%)",
70+
backgroundColor: "#222",
71+
color: "#fff",
72+
padding: "8px 10px",
73+
borderRadius: "6px",
74+
fontSize: "12px",
75+
width: "260px",
76+
zIndex: 1000,
77+
boxShadow: "0 2px 8px rgba(0,0,0,0.2)",
78+
textAlign: "left"
79+
}}
80+
>
81+
A streak increases for every consecutive 7-day period where a user closes at least 3 issues. Streak resets if threshold is not met.
82+
</span>
83+
</span>
84+
</h3>
85+
86+
<ul>
87+
{topContributors.map((user) => (
88+
<li key={user.name}>
89+
{user.name} 🔥 {user.currentStreak}{" "}
90+
{user.currentStreak === 1 ? "week" : "weeks"} streak ({user.count})
91+
</li>
92+
))}
93+
</ul>
94+
</div>
95+
96+
{/* Render list of top repositories by activity count */}
97+
<div>
98+
<h3>Top Repositories</h3>
99+
<ul>
100+
{topRepos.map((repo) => (
101+
<li key={repo.name}>
102+
{repo.name} ({repo.count})
103+
</li>
104+
))}
105+
</ul>
58106
</div>
107+
</div>
59108
);
60109
};
61110

Frontend/src/components/charts/__tests__/test_getTopContributorsAndRepos.test.js

Lines changed: 19 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -12,35 +12,40 @@ import { describe, it, expect } from "vitest";
1212
describe("getTopContributorsAndRepos", () => {
1313
// mock JSON data so that Lint passes
1414
const mockJSON = {
15-
"repo1": {
16-
issues: { "alice": { total_issues_opened: 2 }, "bob": { total_issues_opened: 1 } },
15+
repo1: {
16+
issues: {
17+
alice: { currentStreak: 2, total_issues_closed: 2 },
18+
bob: { currentStreak: 1, total_issues_closed: 1 }
1719
},
18-
"repo2": {
19-
pull_requests: { "charlie": { total_prs_opened: 4 } }
20+
},
21+
repo2: {
22+
issues: {
23+
charlie: { currentStreak: 3, total_issues_closed: 4 }
2024
}
21-
};
25+
}
26+
};
2227

2328

24-
it("counts contributor activity and sorts correctly", () => {
29+
it("sorts contributors based on streak correctly", () => {
2530
const {topContributors} = getTopContributorsAndRepos(mockJSON, 5);
26-
// charlie: 4, alicce: 2, bob: 1
31+
// charlie: 3, alice: 2, bob: 1 (based on streaks in mock data)
2732
expect(topContributors).toEqual(
2833
expect.arrayContaining([
29-
expect.objectContaining({ name: "charlie", count: 4 }),
30-
expect.objectContaining({ name: "alice", count: 2 }),
31-
expect.objectContaining({ name: "bob", count: 1 }),
34+
expect.objectContaining({ name: "charlie", currentStreak: 3 }),
35+
expect.objectContaining({ name: "alice", currentStreak: 2 }),
36+
expect.objectContaining({ name: "bob", currentStreak: 1 }),
3237
])
3338
);
3439
});
3540

36-
it("counts repository activity and sorts correctly", () => {
41+
it("sorts repositories based on streak and active members correctly", () => {
3742
const { topRepos } =
3843
getTopContributorsAndRepos(mockJSON, 5);
3944

40-
// repo1: 3 (2 from alice + 1 from bob), repo2: 4 (from charlie)
45+
// repo2: streak 3 (1 member), repo1: streak 2 (2 members)
4146
expect(topRepos).toEqual([
42-
{ name: "repo2", count: 4 },
43-
{ name: "repo1", count: 3},
47+
{ name: "repo2", streak: 3, activeMembers: 1 },
48+
{ name: "repo1", streak: 2, activeMembers: 2 },
4449
]);
4550
});
4651

Frontend/src/features/home/routes/Home.jsx

Lines changed: 33 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -40,19 +40,33 @@ export const Home = () => {
4040
</section>
4141

4242
<section className="card-blue" style={{ height: "100%" }}>
43-
<h2 className="card-title">Top Contributors/Repos (Issues/PRs)</h2>
43+
<h2 className="card-title">Leaderboard Streaks</h2>
4444
<div className="contributors-wrapper" style={{ display: "flex", gap: "40px", marginTop: "8px" }}>
4545

4646
{/* Top Contributors List */}
4747
<div style={{ flex: 1 }}>
4848
<h3 style={{ fontSize: "16px", fontWeight: "bold", color: "#123f8b", marginBottom: "12px", marginTop: 0 }}>
49-
Top Contributors
49+
Top Contributors{" "}
50+
<span
51+
tabIndex="0"
52+
aria-label="Leaderboard streak info"
53+
title="A streak increases for every consecutive 7-day period where a user closes at least 3 issues. Streak resets if threshold is not met."
54+
style={{
55+
cursor: "pointer",
56+
border: "1px solid black",
57+
borderRadius: "50%",
58+
padding: "2px 6px",
59+
fontSize: "12px"
60+
}}
61+
>
62+
i
63+
</span>
5064
</h3>
5165
<ul style={{ margin: 0, paddingLeft: "20px", color: "#374151" }}>
5266
{topContributors.length > 0 ? (
5367
topContributors.map((user, index) => (
5468
<li key={`user-${index}`} style={{ marginBottom: "4px" }}>
55-
{user.name} ({user.count})
69+
{user.name} 🔥 {user.currentStreak} {user.currentStreak === 1 ? "week" : "weeks"} streak
5670
</li>
5771
))
5872
) : (
@@ -64,13 +78,27 @@ export const Home = () => {
6478
{/* Top Repositories List */}
6579
<div style={{ flex: 1 }}>
6680
<h3 style={{ fontSize: "16px", fontWeight: "bold", color: "#123f8b", marginBottom: "12px", marginTop: 0 }}>
67-
Top Repositories
81+
Top Repositories{" "}
82+
<span
83+
tabIndex="0"
84+
aria-label="Repository members info"
85+
title="Members indicate the number of active contributors contributing to that repository"
86+
style={{
87+
cursor: "pointer",
88+
border: "1px solid black",
89+
borderRadius: "50%",
90+
padding: "2px 6px",
91+
fontSize: "12px"
92+
}}
93+
>
94+
i
95+
</span>
6896
</h3>
6997
<ul style={{ margin: 0, paddingLeft: "20px", color: "#374151" }}>
7098
{topRepos.length > 0 ? (
7199
topRepos.map((repo, index) => (
72100
<li key={`repo-${index}`} style={{ marginBottom: "4px" }}>
73-
{repo.name} ({repo.count})
101+
{repo.name} 🔥 {repo.streak} {repo.streak === 1 ? "week" : "weeks"} streak ({repo.activeMembers} {repo.activeMembers === 1 ? "member" : "members"})
74102
</li>
75103
))
76104
) : (

Frontend/src/utils/getTopContributorsAndRepos.js

Lines changed: 39 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,9 @@ export function getTopContributorsAndRepos(lifetimeData, topN, selectedRepo) {
8080
const contributorStats = {};
8181
const repoStats = {};
8282
const contributorIssuesClosed = {};
83+
const contributorCurrentStreaks = {};
84+
const repoHighestStreaks = {};
85+
const repoActiveStreakMembers = {};
8386

8487
// Safely handle empty data
8588
if (!lifetimeData) return { topContributors: [], topRepos: [] };
@@ -89,6 +92,8 @@ export function getTopContributorsAndRepos(lifetimeData, topN, selectedRepo) {
8992
if (selectedRepo && repoName !== selectedRepo) return;
9093

9194
let repoTotalActivity = 0;
95+
let repoHighestStreak = 0;
96+
let repoActiveMembers = 0;
9297

9398
// Helper to safely add metrics to a user's total and the repo's total
9499
const addActivity = (user, amount, isClosedIssue = false) => {
@@ -117,6 +122,19 @@ export function getTopContributorsAndRepos(lifetimeData, topN, selectedRepo) {
117122

118123
addActivity(user, stats.total_issues_opened);
119124
addActivity(user, stats.total_issues_closed, true);
125+
126+
contributorCurrentStreaks[user] = Math.max(
127+
contributorCurrentStreaks[user] || 0,
128+
Number(stats.currentStreak) || 0
129+
);
130+
131+
const userStreak = Number(stats.currentStreak) || 0;
132+
133+
if (userStreak > 0) {
134+
repoActiveMembers += 1;
135+
}
136+
137+
repoHighestStreak = Math.max(repoHighestStreak, userStreak);
120138
});
121139
}
122140

@@ -137,6 +155,9 @@ export function getTopContributorsAndRepos(lifetimeData, topN, selectedRepo) {
137155
if (repoTotalActivity > 0) {
138156
repoStats[repoName] = (repoStats[repoName] || 0) + repoTotalActivity;
139157
}
158+
159+
repoHighestStreaks[repoName] = repoHighestStreak;
160+
repoActiveStreakMembers[repoName] = repoActiveMembers;
140161
});
141162

142163
// Sort contributors by streak first, then closed issues, then alphabetically
@@ -145,19 +166,28 @@ export function getTopContributorsAndRepos(lifetimeData, topN, selectedRepo) {
145166
name,
146167
count,
147168
totalIssuesClosed: contributorIssuesClosed[name] || 0,
148-
currentStreak: 0 // Placeholder for now
169+
currentStreak: contributorCurrentStreaks[name] || 0
149170
}))
150171
.sort((a, b) =>
151172
b.currentStreak - a.currentStreak ||
152-
b.totalIssuesClosed - a.totalIssuesClosed ||
153-
a.name.localeCompare(b.name))
173+
a.name.localeCompare(b.name)
174+
)
154175
.slice(0, topN);
155176

156-
// Sort repositories by activity volume (descending), then alphabetically
157-
const topRepos = Object.entries(repoStats)
158-
.map(([name, count]) => ({ name, count }))
159-
.sort((a, b) => b.count - a.count || a.name.localeCompare(b.name))
177+
// Sort repositories by highest streak first, then by how many members have a streak, then alphabetically
178+
const topRepos = Object.keys(repoHighestStreaks)
179+
.map((name) => ({
180+
name,
181+
streak: repoHighestStreaks[name] || 0,
182+
activeMembers: repoActiveStreakMembers[name] || 0
183+
}))
184+
.filter((repo) => repo.streak > 0)
185+
.sort((a, b) =>
186+
b.streak - a.streak ||
187+
b.activeMembers - a.activeMembers ||
188+
a.name.localeCompare(b.name)
189+
)
160190
.slice(0, topN);
161-
162-
return { topContributors, topRepos };
191+
192+
return { topContributors, topRepos };
163193
}

0 commit comments

Comments
 (0)