Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions api/test_cc.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import requests
from bs4 import BeautifulSoup

def find_codechef_streak(username):
url = f"https://www.codechef.com/users/{username}"
page = requests.get(url)
soup = BeautifulSoup(page.text, "html.parser")
Comment on lines +4 to +7
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# Check if file exists and examine the specific lines
ls -la api/test_cc.py 2>/dev/null && cat -n api/test_cc.py || echo "File not found"

Repository: OpenLake/Leaderboard-Pro

Length of output: 778


🏁 Script executed:

# Search for all requests.get calls in the api directory
rg -n "requests\.get\(" api -g "*.py" -A 2

Repository: OpenLake/Leaderboard-Pro

Length of output: 5600


🏁 Script executed:

# Also check for timeout usage in general to understand codebase patterns
rg -n "timeout=" api -g "*.py" | head -20

Repository: OpenLake/Leaderboard-Pro

Length of output: 146


Add timeout and error handling for the external request, and move execution from module scope.

Line 6 uses requests.get without a timeout or status handling; this can hang test runs indefinitely and parse error pages as valid data. Additionally, lines 17–18 execute network calls at module import time, which will block during import.

Add timeout, raise_for_status(), and move the function calls into a proper test or main block:

🛠️ Proposed fix
-    page = requests.get(url)
-    soup = BeautifulSoup(page.text, "html.parser")
+    page = requests.get(url, timeout=10)
+    page.raise_for_status()
+    soup = BeautifulSoup(page.text, "html.parser")

And wrap the function calls:

-find_codechef_streak("tourist")
-find_codechef_streak("arpit-mahajan09")
+if __name__ == "__main__":
+    find_codechef_streak("tourist")
+    find_codechef_streak("arpit-mahajan09")
🧰 Tools
🪛 Ruff (0.15.2)

[error] 6-6: Probable use of requests call without timeout

(S113)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@api/test_cc.py` around lines 4 - 7, The function find_codechef_streak
currently calls requests.get without a timeout and without checking the
response, and there are network calls executed at import time; update
find_codechef_streak to call requests.get(url, timeout=...) and immediately call
response.raise_for_status(), wrap the request/parse in a try/except catching
requests.RequestException (and optionally logging or returning None/raising a
clear error), and remove any module-level invocation of find_codechef_streak so
calls happen only inside a test or an if __name__ == "__main__": block or from
the test harness; use the function name find_codechef_streak to locate where to
add timeout, raise_for_status(), and the try/except, and move any direct calls
out of module scope.


# Just grab all text and search
text = soup.get_text()
idx = text.lower().find("streak")

Check failure on line 11 in api/test_cc.py

View workflow job for this annotation

GitHub Actions / Spell checking

`idx` is not a recognized word. (unrecognized-spelling)
if idx != -1:

Check warning on line 12 in api/test_cc.py

View workflow job for this annotation

GitHub Actions / Spell checking

`idx` is not a recognized word. (unrecognized-spelling)
print(text[max(0, idx-100):idx+100])

Check warning on line 13 in api/test_cc.py

View workflow job for this annotation

GitHub Actions / Spell checking

`idx` is not a recognized word. (unrecognized-spelling)

Check warning on line 13 in api/test_cc.py

View workflow job for this annotation

GitHub Actions / Spell checking

`idx` is not a recognized word. (unrecognized-spelling)
else:
print("No streak text found in whole page")

find_codechef_streak("tourist")
find_codechef_streak("arpit-mahajan09")

Check failure on line 18 in api/test_cc.py

View workflow job for this annotation

GitHub Actions / Spell checking

`arpit` is not a recognized word. (unrecognized-spelling)
Comment on lines +17 to +18
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# Find and read the test_cc.py file to see the actual code
fd -t f "test_cc.py" -x cat -n

Repository: OpenLake/Leaderboard-Pro

Length of output: 715


🏁 Script executed:

# Also search for the find_codechef_streak function definition to understand what it does
rg -n "def find_codechef_streak" -A 10

Repository: OpenLake/Leaderboard-Pro

Length of output: 609


Remove network calls from module level.

Lines 17-18 execute requests.get() at import time via find_codechef_streak(), creating side effects and flaky failures. Wrap in a if __name__ == "__main__" guard:

Proposed fix
-find_codechef_streak("tourist")
-find_codechef_streak("arpit-mahajan09")
+if __name__ == "__main__":
+    find_codechef_streak("tourist")
+    find_codechef_streak("arpit-mahajan09")
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@api/test_cc.py` around lines 17 - 18, The module-level calls to
find_codechef_streak("tourist") and find_codechef_streak("arpit-mahajan09")
cause network I/O at import time; move these calls into a runtime guard by
wrapping them in an if __name__ == "__main__": block (i.e., place the two
find_codechef_streak(...) invocations inside that guard) so importing
api.test_cc no longer triggers requests.get() side effects.

20 changes: 20 additions & 0 deletions app/src/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ import { ThemeProvider } from "@/Context/ThemeProvider.jsx";
import { NavMenu } from "./components/NavMenu";
import PublicRoute from "./Context/PublicRoute";
import ContestCalendar from "./components/ContestCalendar";
import Blogs from "./components/Blogs.jsx";
import Achievements from "./components/Achievements.jsx";

const BACKEND = import.meta.env.VITE_BACKEND;

Expand Down Expand Up @@ -166,6 +168,24 @@ function App() {
</PrivateRoute>
}
/>
<Route
exact
path="/blogs"
element={
<PrivateRoute>
<Blogs />
</PrivateRoute>
}
/>
<Route
exact
path="/achievements"
element={
<PrivateRoute>
<Achievements />
</PrivateRoute>
}
/>
<Route exact path="/*" element={<HomePage />} />
</Routes>
<GoToTop />
Expand Down
29 changes: 29 additions & 0 deletions app/src/Context/StreakContext.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { createContext, useState, useContext } from "react";

const StreakContext = createContext();

export const useStreak = () => {
return useContext(StreakContext);
};

export const StreakProvider = ({ children }) => {
const [streaks, setStreaks] = useState({
codeforces: 0,
github: 0,
});

const updateStreak = (platform, streak) => {
setStreaks((prev) => ({
...prev,
[platform]: streak,
}));
};

return (
<StreakContext.Provider value={{ streaks, updateStreak }}>
{children}
</StreakContext.Provider>
);
};

export default StreakContext;
108 changes: 108 additions & 0 deletions app/src/components/AchievementCard.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Progress } from "@/components/ui/progress";
import { cn } from "@/lib/utils";

export function AchievementCard({
title,
description,
icon: Icon,
color,
bg,
tiers,
currentValue,
unlockedTiers
}) {
// Determine highest earned tier
let achievedTierIndex = -1;
let nextRequirement = tiers[0].requirement;
let nextLabel = tiers[0].label;

for (let i = 0; i < tiers.length; i++) {
const tierUnlocked = unlockedTiers?.find(u => u.tier === tiers[i].name);
// Note: We check actual value or unlocked table. We assume the parent component passes `currentValue`
if (tierUnlocked || currentValue >= tiers[i].requirement) {
achievedTierIndex = i;
if (i + 1 < tiers.length) {
nextRequirement = tiers[i+1].requirement;
nextLabel = tiers[i+1].label;
} else {
nextRequirement = tiers[i].requirement; // Maxed out

Check failure on line 29 in app/src/components/AchievementCard.jsx

View workflow job for this annotation

GitHub Actions / Spell checking

`Maxed` is not a recognized word. (unrecognized-spelling)
nextLabel = "Max Tier Earned";
}
} else {
break;
}
}

const isUnlocked = achievedTierIndex >= 0;
const currentTier = isUnlocked ? tiers[achievedTierIndex] : null;
const progressPercent = Math.min(100, Math.max(0, (currentValue / nextRequirement) * 100));

// Determine badge colors based on highest tier
const tierColors = {
Bronze: "text-orange-600 bg-orange-600/10 border-orange-600/20",
Silver: "text-slate-400 bg-slate-400/10 border-slate-400/20",
Gold: "text-yellow-500 bg-yellow-500/10 border-yellow-500/20 shadow-[0_0_10px_rgba(234,179,8,0.3)]",
Platinum: "text-cyan-400 bg-cyan-400/10 border-cyan-400/20 shadow-[0_0_15px_rgba(34,211,238,0.4)]"
};

const badgeStyle = isUnlocked ? tierColors[currentTier.name] : "text-gray-400 bg-gray-400/10 grayscale border-transparent";

// Try to find the timestamp of the highest unlocked
const currentUnlockedLog = isUnlocked ? unlockedTiers?.find(u => u.tier === currentTier.name) : null;
const formattedDate = currentUnlockedLog?.earned_at ? new Date(currentUnlockedLog.earned_at).toLocaleDateString(undefined, {
year: 'numeric', month: 'short', day: 'numeric'
}) : null;

return (
<Card className={cn(
"relative overflow-hidden transition-all duration-300 hover:shadow-md border-muted",
isUnlocked && "border-opacity-50"
)}>
{isUnlocked && (
<div className={cn("absolute -right-12 -top-12 h-32 w-32 rounded-full opacity-20 blur-2xl", bg)} />
)}

<CardHeader className="flex flex-row items-center gap-4 pb-2">
<div className={cn("p-3 rounded-xl border flex-shrink-0 transition-colors duration-500", badgeStyle)}>
<Icon className="h-8 w-8" />
</div>
<div className="flex flex-col">
<CardTitle className="text-lg font-bold">{title}</CardTitle>
<p className="text-xs text-muted-foreground">{description}</p>
</div>
</CardHeader>

<CardContent>
<div className="mt-2 space-y-2">
<div className="flex justify-between text-sm font-medium">
<span className={cn(isUnlocked ? color : "text-muted-foreground")}>
{Math.floor(currentValue)} / {nextRequirement}
</span>
<span className="text-muted-foreground">{nextLabel}</span>
</div>
<Progress
value={progressPercent}
className="h-2 w-full bg-secondary"
indicatorClassName={cn(isUnlocked ? color.replace('text-', 'bg-') : "bg-muted-foreground")}
/>
</div>

<div className="mt-4 flex items-center justify-between text-xs">
{isUnlocked ? (
<>
<span className={cn("font-semibold px-2 py-0.5 rounded-full border", badgeStyle)}>
{currentTier.name}
</span>
<span className="text-muted-foreground text-opacity-75">
{formattedDate ? `Earned ${formattedDate}` : "Earned recently"}
</span>
</>
) : (
<span className="text-muted-foreground italic">Locked</span>
)}
</div>
</CardContent>
</Card>
);
}
177 changes: 177 additions & 0 deletions app/src/components/Achievements.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
import { useState, useEffect } from "react";
import { ACHIEVEMENTS } from "@/utils/achievements";
import { AchievementCard } from "./AchievementCard";
import { useAuth } from "@/Context/AuthContext";
import { useStreak } from "@/Context/StreakContext";

export default function Achievements() {
const { user } = useAuth();
const { globalStreak } = useStreak();
const [stats, setStats] = useState(null);
Comment on lines +8 to +10
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

useStreak() doesn’t expose globalStreak — compute it from streaks.

Line 9 destructures globalStreak, but the context only provides { streaks, updateStreak }. This makes streak-based achievements evaluate to 0. Compute it locally or export globalStreak from the provider.

🛠️ Proposed fix
-  const { globalStreak } = useStreak();
+  const { streaks } = useStreak();
+  const globalStreak = Math.max(0, ...(streaks ? Object.values(streaks) : []));
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const { user } = useAuth();
const { globalStreak } = useStreak();
const [stats, setStats] = useState(null);
const { user } = useAuth();
const { streaks } = useStreak();
const globalStreak = Math.max(0, ...(streaks ? Object.values(streaks) : []));
const [stats, setStats] = useState(null);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/components/Achievements.jsx` around lines 8 - 10, The code
incorrectly destructures globalStreak from useStreak() even though the hook only
provides streaks and updateStreak; fix by computing globalStreak locally in
Achievements.jsx from the returned streaks (e.g., derive the max or sum
depending on intended metric) instead of expecting it from useStreak(), update
any references that use globalStreak to use the locally computed value, and keep
useStreak() usage as const { streaks, updateStreak } = useStreak() while adding
a small helper or inline computation (e.g., computeGlobalStreak(streaks) or
const globalStreak = ...) to produce the correct streak-based achievement
values.

const [unlockedAchievements, setUnlockedAchievements] = useState([]);
const [isLoading, setIsLoading] = useState(true);
const BACKEND = import.meta.env.VITE_BACKEND;

// 1. Fetch user platform stats to evaluate current progress
useEffect(() => {
if (!user) return;

// We can fetch from the endpoints we already have to build a combined `stats` object
// For simplicity, we fetch all 5 endpoints for this user specifically.
// In a real optimized app, we'd have a single /api/user/all_stats/ endpoint.
const fetchPlatformData = async (platform) => {
try {
const res = await fetch(`${BACKEND}/${platform}/`);
const data = await res.json();
// The API returns all users. We need to find this user.
// Usually, the app has a `UserNames` mapping or similar.
// Assuming the auth context has `username` or we can find it:
return data;
} catch (e) {
console.warn(`Failed fetching ${platform} stats`);
return [];
}
};

const fetchAllStats = async () => {
try {
setIsLoading(true);
// We need to fetch the UserNames mapping first to know the platform usernames
const mappingRes = await fetch(`${BACKEND}/usernames/`);
const mappings = await mappingRes.json();
const userMapping = mappings.find(m => m.user === user.user_id);

if (!userMapping) {
setIsLoading(false);
return;
}

const [gh, cf, lc, cc, ac, ol] = await Promise.all([
fetchPlatformData('github'),
fetchPlatformData('codeforces'),
fetchPlatformData('leetcode'),
fetchPlatformData('codechef'),
fetchPlatformData('atcoder'),
fetchPlatformData('openlake')
]);

const combinedStats = {
github: gh.find(u => u.username === userMapping.github),
codeforces: cf.find(u => u.username === userMapping.codeforces),
leetcode: lc.find(u => u.username === userMapping.leetcode),
codechef: cc.find(u => u.username === userMapping.codechef),
atcoder: ac.find(u => u.username === userMapping.atcoder),
openlake: ol.find(u => u.username === userMapping.openlake)
};

setStats(combinedStats);
} catch (error) {
console.error("Error fetching stats for achievements", error);
} finally {
setIsLoading(false);
}
};

// 2. Fetch already unlocked achievements from DB
const fetchUnlocked = async () => {
try {
const res = await fetch(`${BACKEND}/achievements/`, {
headers: { 'Authorization': `Bearer ${user.access}` }
});
if (res.ok) {
const data = await res.json();
setUnlockedAchievements(data);
}
} catch (e) {
console.warn("Failed to fetch unlocked achievements", e);
}
};

fetchAllStats();
fetchUnlocked();
}, [user, BACKEND]);

// 3. Evaluate new unlocks
useEffect(() => {
if (!stats || !user) return;

const unlockAchievement = async (slug, tier) => {
try {
const res = await fetch(`${BACKEND}/achievements/unlock/`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${user.access}`
},
body: JSON.stringify({ slug, tier })
});
if (res.ok) {
const newUnlock = await res.json();
if (!newUnlock.message) { // Meaning it wasn't "Already unlocked"
setUnlockedAchievements(prev => [...prev, newUnlock]);
}
}
} catch(e) {
console.error("Failed to post unlock", e);
}
};

// Check every achievement against requirements
Object.entries(ACHIEVEMENTS).forEach(([slug, def]) => {
const currentValue = def.evaluate(stats, globalStreak);
def.tiers.forEach(tier => {
if (currentValue >= tier.requirement) {
// Check if we already have it
const hasIt = unlockedAchievements.some(u => u.slug === slug && u.tier === tier.name);
if (!hasIt) {
unlockAchievement(slug, tier.name);
}
}
});
});

}, [stats, user, BACKEND, globalStreak, unlockedAchievements]);


if (isLoading) {
return (
<div className="flex justify-center flex-col items-center h-[80vh] w-[100%]">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-primary"></div>
<p className="mt-4 text-muted-foreground animate-pulse">Calculating Tiers...</p>
</div>
);
}

return (
<div className="container mx-auto p-6 md:p-8 space-y-8 animate-in fade-in zoom-in duration-500">
<div>
<h1 className="text-3xl font-bold tracking-tight">Achievements</h1>
<p className="text-muted-foreground mt-2">
Track your progress across Open Source, Competitive Programming, and LeetCode.
Unlock higher tiers to prove your mastery.
</p>
</div>

<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-6">
{Object.entries(ACHIEVEMENTS).map(([slug, def]) => {
const currentValue = stats ? def.evaluate(stats, globalStreak) : 0;
const relevantUnlocks = unlockedAchievements.filter(u => u.slug === slug);

return (
<AchievementCard
key={slug}
title={def.title}
description={def.description}
icon={def.icon}
color={def.color}
bg={def.bg}
tiers={def.tiers}
currentValue={currentValue}
unlockedTiers={relevantUnlocks}
/>
);
})}
</div>
</div>
);
}
Loading
Loading