241 lines
No EOL
20 KiB
HTML
241 lines
No EOL
20 KiB
HTML
<!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.CjyFoliM.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="#58a6ff" 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="#58a6ff" 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() {
|
|
var API_BASE = 'http://localhost:9090/api';
|
|
|
|
// Fallback rank tiers if API doesn't provide rank info
|
|
var rankTiers = [
|
|
{ name: 'Novice', xpThreshold: 0 },
|
|
{ name: 'Apprentice', xpThreshold: 500 },
|
|
{ name: 'Skilled', xpThreshold: 1500 },
|
|
{ name: 'Expert', xpThreshold: 3000 },
|
|
{ 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) {
|
|
return rankTiers[i];
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function setWidth(el, pct) {
|
|
if (!el) return;
|
|
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) {
|
|
if (!res.ok) throw new Error('HTTP ' + res.status);
|
|
return res.json();
|
|
})
|
|
.then(function(user) {
|
|
// Profile header
|
|
var nameEl = document.getElementById('profile-name');
|
|
var titleEl = document.getElementById('profile-title');
|
|
if (nameEl) nameEl.textContent = user.username || user.name || 'Anonymous User';
|
|
if (titleEl) titleEl.textContent = user.title || 'Code Review Practitioner';
|
|
|
|
if (user.avatarUrl) {
|
|
var avatarEl = document.getElementById('avatar-placeholder');
|
|
if (avatarEl) {
|
|
var img = document.createElement('img');
|
|
img.src = user.avatarUrl;
|
|
img.alt = user.username || 'Avatar';
|
|
img.className = 'w-20 h-20 rounded-full object-cover';
|
|
avatarEl.replaceWith(img);
|
|
}
|
|
}
|
|
|
|
// Badges
|
|
var badges = user.badges || user.badge_list || [];
|
|
var badgeList = document.getElementById('badge-list');
|
|
if (badgeList) {
|
|
if (badges.length === 0) {
|
|
badgeList.innerHTML = '<div class="text-center text-[#8b949e] py-4"><p>No badges earned yet</p></div>';
|
|
} else {
|
|
// Tier colors and icons
|
|
var tierMap = {
|
|
'Beginner': { icon: '🌱', color: '#3fb950' },
|
|
'Intermediate': { icon: '🔧', color: '#58a6ff' },
|
|
'Advanced': { icon: '⚡', color: '#d29922' },
|
|
'Expert': { icon: '🏆', color: '#f85149' },
|
|
'Hard Mode': { icon: '💀', color: '#a371f7' },
|
|
'Legendary': { icon: '👑', color: '#f778ba' },
|
|
};
|
|
var defaultTier = { icon: '🏅', color: '#8b949e' };
|
|
|
|
fetch(API_BASE + '/badges')
|
|
.then(function(res) {
|
|
if (!res.ok) throw new Error('HTTP ' + res.status);
|
|
return res.json();
|
|
})
|
|
.then(function(data) {
|
|
var definitions = data.definitions || data.badges || [];
|
|
var badgeMap = {};
|
|
for (var i = 0; i < definitions.length; i++) {
|
|
var def = definitions[i];
|
|
var tierInfo = def.tier || {};
|
|
var tierName = tierInfo.level || tierInfo.name || '';
|
|
var tierData = tierMap[tierName] || defaultTier;
|
|
badgeMap[def.id] = {
|
|
name: def.title || def.id,
|
|
icon: tierData.icon,
|
|
desc: def.description || '',
|
|
color: tierData.color,
|
|
};
|
|
}
|
|
badgeList.innerHTML = '';
|
|
for (var b = 0; b < badges.length; b++) {
|
|
var badgeId = badges[b];
|
|
var meta = badgeMap[badgeId] || { name: badgeId, icon: '🏅', desc: 'A badge earned in challenges', color: '#8b949e' };
|
|
var badgeEl = document.createElement('div');
|
|
badgeEl.className = 'flex items-center gap-3 bg-[#21262d] border border-[#30363d] rounded-md px-4 py-3 min-w-[200px]';
|
|
badgeEl.title = meta.desc;
|
|
badgeEl.innerHTML = '<div class="w-10 h-10 rounded-full flex items-center justify-center text-xl" style="background-color:' + meta.color + '20">' + meta.icon + '</div><div><p class="text-[#c9d1d9] text-sm font-medium">' + meta.name + '</p><p class="text-[#8b949e] text-xs">' + meta.desc + '</p></div>';
|
|
badgeList.appendChild(badgeEl);
|
|
}
|
|
})
|
|
.catch(function(err) {
|
|
console.error('Failed to load badge definitions:', err);
|
|
var fallbackMeta = {
|
|
'first-bug': { name: 'First Bug', icon: '🐛', desc: 'Found your first bug', color: '#3fb950' },
|
|
'rapid-solver': { name: 'Rapid Solver', icon: '⚡', desc: 'Solved 10 challenges quickly', color: '#d29922' },
|
|
'streak-master': { name: 'Streak Master', icon: '🔥', desc: '7-day streak achieved', color: '#f85149' },
|
|
'xp-hunter': { name: 'XP Hunter', icon: '⭐', desc: 'Accumulated 1000 XP', color: '#58a6ff' },
|
|
'bug-slayer': { name: 'Bug Slayer', icon: '⚔️', desc: 'Solved 50 challenges', color: '#a371f7' },
|
|
'perfect-patch': { name: 'Perfect Patch', icon: '🎯', desc: 'Submitted a perfect patch', color: '#f778ba' },
|
|
'code-reviewer': { name: 'Code Reviewer', icon: '📋', desc: 'Completed first code review', color: '#79c0ff' },
|
|
'champion': { name: 'Champion', icon: '🏆', desc: 'Reached Master rank', color: '#d29922' },
|
|
};
|
|
badgeList.innerHTML = '';
|
|
for (var b = 0; b < badges.length; b++) {
|
|
var badgeId = badges[b];
|
|
var meta = fallbackMeta[badgeId] || { name: badgeId, icon: '🏅', desc: 'A badge earned in challenges', color: '#8b949e' };
|
|
var badgeEl = document.createElement('div');
|
|
badgeEl.className = 'flex items-center gap-3 bg-[#21262d] border border-[#30363d] rounded-md px-4 py-3 min-w-[200px]';
|
|
badgeEl.title = meta.desc;
|
|
badgeEl.innerHTML = '<div class="w-10 h-10 rounded-full flex items-center justify-center text-xl" style="background-color:' + meta.color + '20">' + meta.icon + '</div><div><p class="text-[#c9d1d9] text-sm font-medium">' + meta.name + '</p><p class="text-[#8b949e] text-xs">' + meta.desc + '</p></div>';
|
|
badgeList.appendChild(badgeEl);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
// Stats - try snake_case first (API format), then camelCase fallbacks
|
|
var xp = user.total_xp != null ? user.total_xp : (user.xp != null ? user.xp : (user.totalXP != null ? user.totalXP : 0));
|
|
var solved = user.solved_count != null ? user.solved_count : (user.challengesSolved != null ? user.challengesSolved : (user.solvedCount != null ? user.solvedCount : 0));
|
|
var streak = user.streak != null ? user.streak : (user.current_streak != null ? user.current_streak : (user.currentStreak != null ? user.currentStreak : 0));
|
|
|
|
// Fetch total challenges dynamically from API
|
|
var totalChallenges = user.total_challenges != null ? user.total_challenges : (user.totalChallenges != null ? user.totalChallenges : null);
|
|
if (totalChallenges == null) {
|
|
fetch(API_BASE + '/challenges/totalamount')
|
|
.then(function(res) {
|
|
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-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-lg">/?</span>';
|
|
});
|
|
} else {
|
|
// totalChallenges already set from /me response
|
|
}
|
|
|
|
var xpEl = document.getElementById('stat-xp');
|
|
var solvedEl = document.getElementById('stat-solved');
|
|
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');
|
|
var streakPct = (streak / streakNext) * 100;
|
|
if (streakEl) streakEl.innerHTML = streak + '<span class="text-[#8b949e] text-lg">/' + streakNext + '</span>';
|
|
setRingProgress('streak-ring', streakPct);
|
|
|
|
// 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 {
|
|
if (xpDisplayEl) xpDisplayEl.textContent = xp;
|
|
setRingProgress('xp-ring', 100);
|
|
}
|
|
|
|
// Recent Activity
|
|
var activities = user.recentActivity || user.activity || [];
|
|
var activityList = document.getElementById('activity-list');
|
|
if (activityList && activities.length > 0) {
|
|
activityList.innerHTML = '';
|
|
for (var a = 0; a < activities.length && a < 10; a++) {
|
|
var activity = activities[a];
|
|
var item = document.createElement('div');
|
|
item.className = 'flex items-center gap-3 p-3 bg-[#21262d] rounded-md';
|
|
var isSolved = activity.type === 'solved' || activity.type === 'completed';
|
|
var checkSvg = '<svg class="w-5 h-5 text-[#3fb950] flex-shrink-0" fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd"/></svg>';
|
|
var infoSvg = '<svg class="w-5 h-5 text-[#58a6ff] flex-shrink-0" fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" 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" clip-rule="evenodd"/></svg>';
|
|
var xpHtml = activity.xpGained ? '<span class="text-[#58a6ff] text-sm font-medium">+' + activity.xpGained + ' XP</span>' : '';
|
|
item.innerHTML = (isSolved ? checkSvg : infoSvg) + '<div class="flex-1 min-w-0"><p class="text-[#c9d1d9] text-sm truncate">' + (activity.title || activity.description || 'Challenge completed') + '</p><p class="text-[#8b949e] text-xs">' + (activity.date || activity.timestamp || '') + '</p></div>' + xpHtml;
|
|
activityList.appendChild(item);
|
|
}
|
|
}
|
|
})
|
|
.catch(function(err) {
|
|
console.error('Failed to load profile:', err);
|
|
var nameEl = document.getElementById('profile-name');
|
|
if (nameEl) nameEl.textContent = 'Anonymous User';
|
|
var activityList = document.getElementById('activity-list');
|
|
if (activityList) {
|
|
activityList.innerHTML = '<div class="text-center text-[#f85149] py-4">Failed to load profile data. Is the API running?</div>';
|
|
}
|
|
});
|
|
}
|
|
|
|
loadProfile();
|
|
})();
|
|
</script> <footer class="border-t border-[#30363d] mt-16"> <div class="max-w-6xl mx-auto px-4 py-12"> <div class="grid grid-cols-2 md:grid-cols-4 gap-8"> <div> <h3 class="text-[#c9d1d9] font-semibold mb-4">PR Dojo</h3> <ul class="space-y-2"> <li> <a href="/" class="text-[#8b949e] text-sm hover:text-[#c9d1d9]">Challenges</a> </li> <li> <a href="/profile" class="text-[#8b949e] text-sm hover:text-[#c9d1d9]">Profile</a> </li> </ul> </div> <div> <h3 class="text-[#c9d1d9] font-semibold mb-4">Learn</h3> <ul class="space-y-2"> <li> <a href="/about" class="text-[#8b949e] text-sm hover:text-[#c9d1d9]">About</a> </li> <li> <a href="/faq" class="text-[#8b949e] text-sm hover:text-[#c9d1d9]">FAQ</a> </li> </ul> </div> <div> <h3 class="text-[#c9d1d9] font-semibold mb-4">Legal</h3> <ul class="space-y-2"> <li> <a href="/imprint" class="text-[#8b949e] text-sm hover:text-[#c9d1d9]">Imprint</a> </li> <li> <a href="/privacy" class="text-[#8b949e] text-sm hover:text-[#c9d1d9]">Privacy Policy</a> </li> <li> <a href="/terms" class="text-[#8b949e] text-sm hover:text-[#c9d1d9]">Terms of Service</a> </li> </ul> </div> <div> <h3 class="text-[#c9d1d9] font-semibold mb-4">Connect</h3> <ul class="space-y-2"> <li> <a href="https://github.com" target="_blank" rel="noopener noreferrer" class="text-[#8b949e] text-sm hover:text-[#c9d1d9]">GitHub</a> </li> <li> <a href="https://twitter.com" target="_blank" rel="noopener noreferrer" class="text-[#8b949e] text-sm hover:text-[#c9d1d9]">Twitter</a> </li> </ul> </div> </div> <div class="border-t border-[#30363d] mt-8 pt-8 text-center"> <p class="text-[#8b949e] text-sm">© 2026 PR Dojo. All rights reserved.</p> </div> </div> </footer></body></html> |