Convert horizontal profile statistic bars to circular ones

This commit is contained in:
Oracle 2026-05-02 19:05:26 +02:00
parent c17d6b6b70
commit d9c27b4353
Signed by: Oracle
SSH key fingerprint: SHA256:x4/RtnjUyuHkdvmwNDsWSfcfF1V5PNr3OpriZqOvCX8
18 changed files with 167 additions and 191 deletions

View file

@ -1,4 +1,4 @@
<!DOCTYPE html><html lang="en" class="dark" data-astro-cid-sckkx6r4> <head><meta charset="UTF-8"><meta name="viewport" content="width=device-width"><link rel="icon" type="image/svg+xml" href="/favicon.svg"><meta name="generator" content="Astro v6.1.7"><title>PR Dojo - Code Review Practice</title><link rel="stylesheet" href="/_astro/Layout.slfrh7tA.css"></head> <body data-astro-cid-sckkx6r4> <div class="min-h-screen"> <!-- Header --> <header class="bg-[#161b22] border-b border-[#30363d]"> <div class="max-w-6xl mx-auto px-4 py-3"> <a href="/" class="text-[#58a6ff] text-sm font-medium no-underline focus:outline-none focus:ring-0">← Back to Challenges</a> </div> </header> <main class="max-w-6xl mx-auto px-4 py-6"> <!-- Profile Header --> <div class="bg-[#161b22] border border-[#30363d] rounded-md p-6 mb-6"> <div class="flex items-center gap-6"> <div class="w-20 h-20 bg-[#21262d] rounded-full flex items-center justify-center"> <svg id="avatar-placeholder" class="w-12 h-12 text-[#8b949e]" fill="currentColor" viewBox="0 0 20 20"> <path fill-rule="evenodd" d="M10 9a3 3 0 100-6 3 3 0 000 6zm-7 9a7 7 0 1114 0H3z" clip-rule="evenodd"></path> </svg> </div> <div> <h1 id="profile-name" class="text-2xl font-semibold text-[#c9d1d9]">Loading...</h1> <p id="profile-title" class="text-[#8b949e] text-sm mt-1">Code Review Practitioner</p> </div> </div> </div> <!-- Stats Grid --> <div class="grid grid-cols-1 md:grid-cols-3 gap-4 mb-6"> <div class="bg-[#161b22] border border-[#30363d] rounded-md p-5"> <div class="flex items-center gap-3 mb-2"> <svg class="w-6 h-6 text-[#58a6ff]" fill="currentColor" viewBox="0 0 20 20"> <path d="M10 18a8 8 0 100-16 8 8 0 000 16zm1-11a1 1 0 10-2 0v2H7a1 1 0 100 2h2v2a1 1 0 102 0v-2h2a1 1 0 100-2h-2V7z"></path> </svg> <span class="text-[#8b949e] text-sm">Total XP</span> </div> <div id="stat-xp" class="text-3xl font-bold text-[#58a6ff]">?</div> </div> <div class="bg-[#161b22] border border-[#30363d] rounded-md p-5"> <div class="flex items-center gap-3 mb-2"> <svg class="w-6 h-6 text-[#a5d6ff]" fill="currentColor" viewBox="0 0 20 20"> <path fill-rule="evenodd" d="M6.267 3.455a3.066 3.066 0 001.745-.723 3.066 3.066 0 013.976 0 3.066 3.066 0 001.745.723 3.066 3.066 0 012.812 2.812c.051.643.304 1.254.723 1.745a3.066 3.066 0 010 3.976 3.066 3.066 0 00-.723 1.745 3.066 3.066 0 01-2.812 2.812 3.066 3.066 0 00-1.745.723 3.066 3.066 0 01-3.976 0 3.066 3.066 0 00-1.745-.723 3.066 3.066 0 01-2.812-2.812 3.066 3.066 0 00-.723-1.745 3.066 3.066 0 010-3.976 3.066 3.066 0 00.723-1.745 3.066 3.066 0 012.812-2.812zm7.44 5.252a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd"></path> </svg> <span class="text-[#8b949e] text-sm">Challenges Solved</span> </div> <div id="stat-solved" class="text-3xl font-bold text-[#c9d1d9]">?/<span class="text-[#8b949e] text-xl">?</span></div> </div> <div class="bg-[#161b22] border border-[#30363d] rounded-md p-5"> <div class="flex items-center gap-3 mb-2"> <svg class="w-6 h-6 text-[#79c0ff]" fill="currentColor" viewBox="0 0 20 20"> <path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm1-12a1 1 0 10-2 0v4a1 1 0 00.293.707l2.828 2.829a1 1 0 101.415-1.415L11 9.586V6z" clip-rule="evenodd"></path> </svg> <span class="text-[#8b949e] text-sm">Current Streak</span> </div> <div id="stat-streak" class="text-3xl font-bold text-[#79c0ff]">?<span class="text-[#8b949e] text-xl"> days</span></div> </div> </div> <!-- Rank Progress --> <div class="bg-[#161b22] border border-[#30363d] rounded-md p-6 mb-6"> <div class="flex items-center justify-between mb-3"> <span id="rank-label" class="text-[#c9d1d9] font-semibold">Loading rank...</span> <span id="rank-xp" class="text-[#8b949e] text-sm">? / ? XP</span> </div> <div class="h-3 bg-[#21262d] rounded-full overflow-hidden"> <div id="rank-bar" class="h-full bg-[#58a6ff] w-0 transition-all duration-500"></div> </div> </div> <!-- Badges --> <div class="bg-[#161b22] border border-[#30363d] rounded-md p-6 mb-6"> <h2 class="text-lg font-semibold text-[#c9d1d9] mb-4">Badges</h2> <div id="badge-list" class="flex flex-wrap gap-3"> <div class="text-center text-[#8b949e] py-4"> <p>Loading badges...</p> </div> </div> </div> <!-- Recent Activity --> <div class="bg-[#161b22] border border-[#30363d] rounded-md p-6"> <h2 class="text-lg font-semibold text-[#c9d1d9] mb-4">Recent Activity</h2> <div id="activity-list" class="space-y-3"> <div class="text-center text-[#8b949e] py-8"> <svg class="w-12 h-12 mx-auto mb-3 text-[#30363d]" fill="currentColor" viewBox="0 0 20 20"> <path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm1-12a1 1 0 10-2 0v4a1 1 0 00.293.707l2.828 2.829a1 1 0 101.415-1.415L11 9.586V6z" clip-rule="evenodd"></path> </svg> <p>No challenges completed yet</p> <a href="/challenges/1" class="text-[#58a6ff] mt-2 inline-block no-underline">
<!DOCTYPE html><html lang="en" class="dark" data-astro-cid-sckkx6r4> <head><meta charset="UTF-8"><meta name="viewport" content="width=device-width"><link rel="icon" type="image/svg+xml" href="/favicon.svg"><meta name="generator" content="Astro v6.1.7"><title>PR Dojo - Code Review Practice</title><link rel="stylesheet" href="/_astro/Layout.CLiT21to.css"></head> <body data-astro-cid-sckkx6r4> <div class="min-h-screen"> <!-- Header --> <header class="bg-[#161b22] border-b border-[#30363d]"> <div class="max-w-6xl mx-auto px-4 py-3"> <a href="/" class="text-[#58a6ff] text-sm font-medium no-underline focus:outline-none focus:ring-0">← Back to Challenges</a> </div> </header> <main class="max-w-6xl mx-auto px-4 py-6"> <!-- Profile Header --> <div class="bg-[#161b22] border border-[#30363d] rounded-md p-6 mb-6"> <div class="flex items-center gap-6"> <div class="w-20 h-20 bg-[#21262d] rounded-full flex items-center justify-center"> <svg id="avatar-placeholder" class="w-12 h-12 text-[#8b949e]" fill="currentColor" viewBox="0 0 20 20"> <path fill-rule="evenodd" d="M10 9a3 3 0 100-6 3 3 0 000 6zm-7 9a7 7 0 1114 0H3z" clip-rule="evenodd"></path> </svg> </div> <div> <h1 id="profile-name" class="text-2xl font-semibold text-[#c9d1d9]">Loading...</h1> <p id="profile-title" class="text-[#8b949e] text-sm mt-1">Code Review Practitioner</p> </div> </div> </div> <!-- Circular Progress Bars --> <div class="grid grid-cols-1 md:grid-cols-3 gap-6 mb-6"> <!-- XP Progress --> <div class="bg-[#161b22] border border-[#30363d] rounded-md p-6 flex flex-col items-center"> <h3 class="text-[#8b949e] text-sm font-medium mb-4">Total XP</h3> <div class="relative w-36 h-36"> <svg class="w-36 h-36 transform -rotate-90" viewBox="0 0 120 120"> <circle cx="60" cy="60" r="52" fill="none" stroke="#21262d" stroke-width="10"></circle> <circle cx="60" cy="60" r="52" fill="none" stroke="#58a6ff" stroke-width="10" stroke-dasharray="326.73" stroke-dashoffset="326.73" stroke-linecap="round" id="xp-ring" class="transition-all duration-700 ease-out"></circle> </svg> <div class="absolute inset-0 flex flex-col items-center justify-center"> <span id="stat-xp" class="text-2xl font-bold text-[#58a6ff]">?/?</span> <span class="text-[#8b949e] text-xs mt-1">XP</span> </div> </div> </div> <!-- Challenges Solved --> <div class="bg-[#161b22] border border-[#30363d] rounded-md p-6 flex flex-col items-center"> <h3 class="text-[#8b949e] text-sm font-medium mb-4">Challenges Solved</h3> <div class="relative w-36 h-36"> <svg class="w-36 h-36 transform -rotate-90" viewBox="0 0 120 120"> <circle cx="60" cy="60" r="52" fill="none" stroke="#21262d" stroke-width="10"></circle> <circle cx="60" cy="60" r="52" fill="none" stroke="#a5d6ff" stroke-width="10" stroke-dasharray="326.73" stroke-dashoffset="326.73" stroke-linecap="round" id="solved-ring" class="transition-all duration-700 ease-out"></circle> </svg> <div class="absolute inset-0 flex flex-col items-center justify-center"> <span id="stat-solved" class="text-2xl font-bold text-[#c9d1d9]">?/<span class="text-[#8b949e] text-lg">?</span></span> <span class="text-[#8b949e] text-xs mt-1">Solved</span> </div> </div> </div> <!-- Streak Progress --> <div class="bg-[#161b22] border border-[#30363d] rounded-md p-6 flex flex-col items-center"> <h3 class="text-[#8b949e] text-sm font-medium mb-4">Current Streak</h3> <div class="relative w-36 h-36"> <svg class="w-36 h-36 transform -rotate-90" viewBox="0 0 120 120"> <circle cx="60" cy="60" r="52" fill="none" stroke="#21262d" stroke-width="10"></circle> <circle cx="60" cy="60" r="52" fill="none" stroke="#79c0ff" stroke-width="10" stroke-dasharray="326.73" stroke-dashoffset="326.73" stroke-linecap="round" id="streak-ring" class="transition-all duration-700 ease-out"></circle> </svg> <div class="absolute inset-0 flex flex-col items-center justify-center"> <span id="stat-streak" class="text-3xl font-bold text-[#79c0ff]">?<span class="text-[#8b949e] text-lg">d</span></span> <span class="text-[#8b949e] text-xs mt-1">days</span> </div> </div> </div> </div> <!-- Badges --> <div class="bg-[#161b22] border border-[#30363d] rounded-md p-6 mb-6"> <h2 class="text-lg font-semibold text-[#c9d1d9] mb-4">Badges</h2> <div id="badge-list" class="flex flex-wrap gap-3"> <div class="text-center text-[#8b949e] py-4"> <p>Loading badges...</p> </div> </div> </div> <!-- Recent Activity --> <div class="bg-[#161b22] border border-[#30363d] rounded-md p-6"> <h2 class="text-lg font-semibold text-[#c9d1d9] mb-4">Recent Activity</h2> <div id="activity-list" class="space-y-3"> <div class="text-center text-[#8b949e] py-8"> <svg class="w-12 h-12 mx-auto mb-3 text-[#30363d]" fill="currentColor" viewBox="0 0 20 20"> <path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm1-12a1 1 0 10-2 0v4a1 1 0 00.293.707l2.828 2.829a1 1 0 101.415-1.415L11 9.586V6z" clip-rule="evenodd"></path> </svg> <p>No challenges completed yet</p> <a href="/challenges/1" class="text-[#58a6ff] mt-2 inline-block no-underline">
Start your first challenge →
</a> </div> </div> </div> </main> </div> <script client:load>
(function() {
@ -13,6 +13,8 @@ Start your first challenge →
{ name: 'Master', xpThreshold: 5000 },
];
var streakTiers = [1, 3, 5, 10, 20, 30];
function getNextTier(currentThreshold) {
for (var i = 0; i < rankTiers.length; i++) {
if (rankTiers[i].xpThreshold > currentThreshold) {
@ -27,6 +29,15 @@ Start your first challenge →
el.style.width = Math.min(100, Math.max(0, pct)) + '%';
}
var CIRCUMFERENCE = 2 * Math.PI * 52; // ~326.73
function setRingProgress(ringId, pct) {
var ring = document.getElementById(ringId);
if (!ring) return;
var offset = CIRCUMFERENCE - (Math.min(100, Math.max(0, pct)) / 100) * CIRCUMFERENCE;
ring.style.strokeDashoffset = offset;
}
function loadProfile() {
fetch(API_BASE + '/me')
.then(function(res) {
@ -139,89 +150,61 @@ Start your first challenge →
if (!res.ok) throw new Error('HTTP ' + res.status);
return res.json();
})
.then(function(data) {
var total = typeof data === 'number' ? data : (data.total_amount != null ? data.total_amount : (data.totalAmount != null ? data.totalAmount : (data.total != null ? data.total : '?')));
var solvedEl = document.getElementById('stat-solved');
if (solvedEl) solvedEl.innerHTML = solved + '<span class="text-[#8b949e] text-xl">/' + total + '</span>';
})
.then(function(data) {
var total = typeof data === 'number' ? data : (data.total_amount != null ? data.total_amount : (data.totalAmount != null ? data.totalAmount : (data.total != null ? data.total : '?')));
var solvedEl = document.getElementById('stat-solved');
if (solvedEl) solvedEl.innerHTML = solved + '<span class="text-[#8b949e] text-lg">/' + total + '</span>';
if (total && total > 0) {
setRingProgress('solved-ring', (solved / total) * 100);
}
})
.catch(function(err) {
console.error('Failed to load total challenges:', err);
var solvedEl = document.getElementById('stat-solved');
if (solvedEl) solvedEl.innerHTML = solved + '<span class="text-[#8b949e] text-xl">/?</span>';
if (solvedEl) solvedEl.innerHTML = solved + '<span class="text-[#8b949e] text-lg">/?</span>';
});
} else {
// totalChallenges already set from /me response
}
var xpEl = document.getElementById('stat-xp');
if (xpEl) xpEl.textContent = String(xp);
var solvedEl = document.getElementById('stat-solved');
if (solvedEl) solvedEl.innerHTML = solved + '<span class="text-[#8b949e] text-xl">/' + totalChallenges + '</span>';
if (solvedEl) solvedEl.innerHTML = solved + '<span class="text-[#8b949e] text-lg">/' + totalChallenges + '</span>';
var totalForSolved = totalChallenges != null && totalChallenges > 0 ? totalChallenges : 1;
setRingProgress('solved-ring', (solved / totalForSolved) * 100);
var streakNext = streakTiers[0];
for (var i = streakTiers.length - 1; i >= 0; i--) {
if (streak >= streakTiers[i]) {
streakNext = streakTiers[i + 1] || streakTiers[i];
break;
}
}
var streakEl = document.getElementById('stat-streak');
if (streakEl) streakEl.innerHTML = streak + '<span class="text-[#8b949e] text-xl"> days</span>';
var streakPct = (streak / streakNext) * 100;
if (streakEl) streakEl.innerHTML = streak + '<span class="text-[#8b949e] text-lg">/' + streakNext + '</span>';
setRingProgress('streak-ring', streakPct);
// Rank - use API rank object if available, otherwise compute from tiers
var rankLabelEl = document.getElementById('rank-label');
var rankXpEl = document.getElementById('rank-xp');
var rankBar = document.getElementById('rank-bar');
if (user.rank && user.rank.title) {
// API provides rank object
var apiRank = user.rank;
var rankTitle = apiRank.title;
var currentXp = apiRank.current_xp != null ? apiRank.current_xp : xp;
var currentThreshold = apiRank.xp_threshold != null ? apiRank.xp_threshold : 0;
// Find next tier from fallback list
var nextTier = getNextTier(currentThreshold);
if (nextTier) {
var range = nextTier.xpThreshold - currentThreshold;
var pct = range > 0 ? ((currentXp - currentThreshold) / range) * 100 : 100;
if (rankLabelEl) rankLabelEl.textContent = 'Current Rank: ' + rankTitle + ' \u2192 Next: ' + nextTier.name;
if (rankXpEl) rankXpEl.textContent = currentXp + ' / ' + nextTier.xpThreshold + ' XP';
if (rankBar) {
rankBar.className = 'h-full bg-[#58a6ff] transition-all duration-500';
setWidth(rankBar, pct);
}
} else {
if (rankLabelEl) rankLabelEl.textContent = 'Current Rank: ' + rankTitle;
if (rankXpEl) rankXpEl.textContent = currentXp + ' XP';
if (rankBar) {
rankBar.className = 'h-full bg-[#58a6ff] transition-all duration-500';
setWidth(rankBar, 100);
}
// XP ring with rank threshold
var currentTier = rankTiers[0];
var nextTier = rankTiers[1];
for (var i = rankTiers.length - 1; i >= 0; i--) {
if (xp >= rankTiers[i].xpThreshold) {
currentTier = rankTiers[i];
nextTier = rankTiers[i + 1] || null;
break;
}
}
var xpDisplayEl = document.getElementById('stat-xp');
if (nextTier) {
var xpPct = (xp / nextTier.xpThreshold) * 100;
if (xpDisplayEl) xpDisplayEl.innerHTML = xp + '<span class="text-[#8b949e] text-lg">/' + nextTier.xpThreshold + '</span>';
setRingProgress('xp-ring', xpPct);
} else {
// No rank from API, compute from tiers
var currentTier = rankTiers[0];
var nextTier = rankTiers[1];
for (var i = rankTiers.length - 1; i >= 0; i--) {
if (xp >= rankTiers[i].xpThreshold) {
currentTier = rankTiers[i];
nextTier = rankTiers[i + 1] || null;
break;
}
}
if (nextTier) {
var range = nextTier.xpThreshold - currentTier.xpThreshold;
var pct = range > 0 ? ((xp - currentTier.xpThreshold) / range) * 100 : 100;
if (rankLabelEl) rankLabelEl.textContent = 'Current Rank: ' + currentTier.name + ' \u2192 Next: ' + nextTier.name;
if (rankXpEl) rankXpEl.textContent = xp + ' / ' + nextTier.xpThreshold + ' XP';
if (rankBar) {
rankBar.className = 'h-full bg-[#58a6ff] transition-all duration-500';
setWidth(rankBar, pct);
}
} else {
if (rankLabelEl) rankLabelEl.textContent = 'Current Rank: ' + currentTier.name;
if (rankXpEl) rankXpEl.textContent = xp + ' XP';
if (rankBar) {
rankBar.className = 'h-full bg-[#58a6ff] transition-all duration-500';
setWidth(rankBar, 100);
}
}
if (xpDisplayEl) xpDisplayEl.textContent = xp;
setRingProgress('xp-ring', 100);
}
// Recent Activity