-
Notifications
You must be signed in to change notification settings - Fork 58
Resolved bugs in streaks, Added Blog and Achievement section #190
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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") | ||
|
|
||
| # Just grab all text and search | ||
| text = soup.get_text() | ||
| idx = text.lower().find("streak") | ||
| if idx != -1: | ||
| print(text[max(0, idx-100):idx+100]) | ||
|
Check warning on line 13 in api/test_cc.py
|
||
| else: | ||
| print("No streak text found in whole page") | ||
|
|
||
| find_codechef_streak("tourist") | ||
| find_codechef_streak("arpit-mahajan09") | ||
|
Comment on lines
+17
to
+18
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 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 -nRepository: 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 10Repository: OpenLake/Leaderboard-Pro Length of output: 609 Remove network calls from module level. Lines 17-18 execute 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 |
||
| 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; |
| 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 | ||
| 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> | ||
| ); | ||
| } | ||
| 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
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Line 9 destructures 🛠️ Proposed fix- const { globalStreak } = useStreak();
+ const { streaks } = useStreak();
+ const globalStreak = Math.max(0, ...(streaks ? Object.values(streaks) : []));📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||
| 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> | ||||||||||||||||
| ); | ||||||||||||||||
| } | ||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
Repository: OpenLake/Leaderboard-Pro
Length of output: 778
🏁 Script executed:
Repository: OpenLake/Leaderboard-Pro
Length of output: 5600
🏁 Script executed:
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.getwithout 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
And wrap the function calls:
🧰 Tools
🪛 Ruff (0.15.2)
[error] 6-6: Probable use of
requestscall without timeout(S113)
🤖 Prompt for AI Agents