step 8.3: done
This commit is contained in:
@@ -660,3 +660,137 @@ button:hover { background: #c73650; }
|
||||
background: #555;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* ===== Profile page ===== */
|
||||
.profile-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
margin-bottom: 1.25rem;
|
||||
padding-bottom: 0.75rem;
|
||||
border-bottom: 1px solid #333;
|
||||
}
|
||||
|
||||
.profile-header h2 {
|
||||
color: #e94560;
|
||||
font-size: 1.5rem;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.profile-class-badge {
|
||||
font-size: 0.875rem;
|
||||
color: #a0a0a0;
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
.profile-level-badge {
|
||||
font-size: 0.875rem;
|
||||
color: #f2c14e;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.profile-summary {
|
||||
display: flex;
|
||||
gap: 2rem;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 1.25rem;
|
||||
padding: 0.75rem;
|
||||
background: #0f3460;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.summary-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.2rem;
|
||||
}
|
||||
|
||||
.summary-label {
|
||||
font-size: 0.7rem;
|
||||
color: #a0a0a0;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
.summary-value {
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
color: #eadfce;
|
||||
}
|
||||
|
||||
.gold-value {
|
||||
color: #f2c14e;
|
||||
}
|
||||
|
||||
.profile-body {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.profile-body {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.profile-stats h3,
|
||||
.profile-equipment h3 {
|
||||
color: #e94560;
|
||||
font-size: 0.95rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.profile-stat-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0.35rem 0.5rem;
|
||||
background: #0f3460;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 0.35rem;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.profile-stat-label {
|
||||
color: #a0a0a0;
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
.profile-stat-value {
|
||||
font-weight: 600;
|
||||
color: #eadfce;
|
||||
}
|
||||
|
||||
.profile-equip-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.35rem;
|
||||
}
|
||||
|
||||
.profile-equip-slot {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0.5rem 0.75rem;
|
||||
background: #0f3460;
|
||||
border-radius: 4px;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.profile-slot-label {
|
||||
color: #a0a0a0;
|
||||
text-transform: capitalize;
|
||||
min-width: 3.5em;
|
||||
}
|
||||
|
||||
.profile-slot-item {
|
||||
font-weight: 600;
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
.profile-slot-empty {
|
||||
color: #666;
|
||||
font-style: italic;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
// Profile page: public hero profile lookup via ?name= query param.
|
||||
|
||||
const ITEM_EMOJI = { weapon: '⚔️', armor: '🛡️', trinket: '💍', consumable: '🧪', material: '📦' };
|
||||
|
||||
// ─── API helper (no auth needed) ──────────────────────────────────────────
|
||||
|
||||
async function apiGet(path) {
|
||||
const res = await fetch(path);
|
||||
return res.json();
|
||||
}
|
||||
|
||||
// ─── Display helpers ──────────────────────────────────────────────────────
|
||||
|
||||
function formatKd(kills, deaths) {
|
||||
if (deaths === 0) return kills > 0 ? `${kills}.00` : '—';
|
||||
return (kills / deaths).toFixed(2);
|
||||
}
|
||||
|
||||
// ─── Render: profile ─────────────────────────────────────────────────────
|
||||
|
||||
function renderProfile(data) {
|
||||
document.getElementById('profile-loading').style.display = 'none';
|
||||
document.getElementById('profile-content').style.display = '';
|
||||
|
||||
document.getElementById('profile-name').textContent = data.name;
|
||||
|
||||
const classEl = document.getElementById('profile-class');
|
||||
classEl.textContent = data.class;
|
||||
classEl.className = `profile-class-badge rarity-${data.class}`;
|
||||
|
||||
document.getElementById('profile-level').textContent = `Lv.${data.level}`;
|
||||
document.getElementById('profile-gold').textContent = `💰 ${data.gold.toLocaleString()}`;
|
||||
document.getElementById('profile-kills').textContent = data.kills;
|
||||
document.getElementById('profile-deaths').textContent = data.deaths;
|
||||
document.getElementById('profile-kd').textContent = formatKd(data.kills, data.deaths);
|
||||
|
||||
// Stats
|
||||
const statsList = document.getElementById('profile-stats-list');
|
||||
statsList.innerHTML = '';
|
||||
for (const stat of ['str', 'agi', 'int', 'vit']) {
|
||||
const row = document.createElement('div');
|
||||
row.className = 'profile-stat-row';
|
||||
row.innerHTML = `
|
||||
<span class="profile-stat-label">${stat}</span>
|
||||
<span class="profile-stat-value">${data[stat]}</span>
|
||||
`;
|
||||
statsList.appendChild(row);
|
||||
}
|
||||
|
||||
// Equipment
|
||||
const equipList = document.getElementById('profile-equipment-list');
|
||||
equipList.innerHTML = '';
|
||||
for (const slot of ['weapon', 'armor', 'trinket']) {
|
||||
const slotEl = document.createElement('div');
|
||||
slotEl.className = 'profile-equip-slot';
|
||||
|
||||
const label = document.createElement('span');
|
||||
label.className = 'profile-slot-label';
|
||||
label.textContent = slot;
|
||||
slotEl.appendChild(label);
|
||||
|
||||
const item = data.equipment[slot];
|
||||
if (item) {
|
||||
const value = document.createElement('span');
|
||||
value.className = `profile-slot-item rarity-${getRarityForSlot(slot, data)}`;
|
||||
value.textContent = `${ITEM_EMOJI[slot] || ''} ${item}`;
|
||||
slotEl.appendChild(value);
|
||||
} else {
|
||||
const empty = document.createElement('span');
|
||||
empty.className = 'profile-slot-empty';
|
||||
empty.textContent = 'Empty';
|
||||
slotEl.appendChild(empty);
|
||||
}
|
||||
|
||||
equipList.appendChild(slotEl);
|
||||
}
|
||||
}
|
||||
|
||||
function getRarityForSlot(slot, data) {
|
||||
// Fallback: use class name for colour when item name is present but we don't have rarity info.
|
||||
return data.class || 'common';
|
||||
}
|
||||
|
||||
// ─── Init ─────────────────────────────────────────────────────────────────
|
||||
|
||||
async function init() {
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const name = params.get('name');
|
||||
|
||||
if (!name) {
|
||||
document.getElementById('profile-loading').style.display = 'none';
|
||||
document.getElementById('profile-error').style.display = '';
|
||||
document.getElementById('profile-error').textContent = 'No hero name provided. Use ?name=HeroName.';
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const data = await apiGet(`/api/heroes/${encodeURIComponent(name)}`);
|
||||
if (!data) {
|
||||
throw new Error('Empty response');
|
||||
}
|
||||
if (!data.ok) {
|
||||
if (data.error?.code === 'NOT_FOUND') {
|
||||
document.getElementById('profile-loading').style.display = 'none';
|
||||
document.getElementById('profile-notfound').style.display = '';
|
||||
} else {
|
||||
throw new Error(data.error?.message || 'Failed to load profile.');
|
||||
}
|
||||
return;
|
||||
}
|
||||
renderProfile(data.data);
|
||||
} catch (err) {
|
||||
document.getElementById('profile-loading').style.display = 'none';
|
||||
document.getElementById('profile-error').style.display = '';
|
||||
document.getElementById('profile-error').textContent = err.message || 'Failed to load profile.';
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', init);
|
||||
@@ -0,0 +1,76 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Emberfall — Hero Profile</title>
|
||||
<link rel="stylesheet" href="/css/main.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="game-container">
|
||||
<!-- Top bar -->
|
||||
<header class="top-bar">
|
||||
<div class="logo">Emberfall</div>
|
||||
<div class="user-info">
|
||||
<a href="/login.html">Login</a>
|
||||
<a href="/leaderboard.html">Leaderboard</a>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main class="main-content">
|
||||
<!-- Hero profile panel -->
|
||||
<section class="panel profile-panel" style="grid-column: 1 / -1;">
|
||||
<div id="profile-loading" class="loading">Loading hero profile...</div>
|
||||
<div id="profile-content" style="display:none;">
|
||||
<div class="profile-header">
|
||||
<h2 id="profile-name"></h2>
|
||||
<span id="profile-class" class="profile-class-badge"></span>
|
||||
<span id="profile-level" class="profile-level-badge"></span>
|
||||
</div>
|
||||
|
||||
<div class="profile-summary">
|
||||
<div class="summary-item">
|
||||
<span class="summary-label">Gold</span>
|
||||
<span id="profile-gold" class="summary-value gold-value"></span>
|
||||
</div>
|
||||
<div class="summary-item">
|
||||
<span class="summary-label">Kills</span>
|
||||
<span id="profile-kills" class="summary-value"></span>
|
||||
</div>
|
||||
<div class="summary-item">
|
||||
<span class="summary-label">Deaths</span>
|
||||
<span id="profile-deaths" class="summary-value"></span>
|
||||
</div>
|
||||
<div class="summary-item">
|
||||
<span class="summary-label">Kill/Death</span>
|
||||
<span id="profile-kd" class="summary-value"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="profile-body">
|
||||
<!-- Stats column -->
|
||||
<div class="profile-stats">
|
||||
<h3>Attributes</h3>
|
||||
<div id="profile-stats-list"></div>
|
||||
</div>
|
||||
|
||||
<!-- Equipment column -->
|
||||
<div class="profile-equipment">
|
||||
<h3>Equipment</h3>
|
||||
<div id="profile-equipment-list" class="profile-equip-list"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="profile-error" style="display:none;" class="error"></div>
|
||||
<div id="profile-notfound" style="display:none;" class="placeholder">
|
||||
Hero not found. Check the name and try again.
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<script src="/js/auth.js"></script>
|
||||
<script src="/js/profile.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user