PR-Dojo/dist/profile/index.html

258 lines
No EOL
21 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.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">
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 },
];
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)) + '%';
}
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-xl">/' + total + '</span>';
})
.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>';
});
} 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>';
var streakEl = document.getElementById('stat-streak');
if (streakEl) streakEl.innerHTML = streak + '<span class="text-[#8b949e] text-xl"> days</span>';
// 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);
}
}
} 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);
}
}
}
// 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">&copy; 2026 PR Dojo. All rights reserved.</p> </div> </div> </footer></body></html>