Files
ShooterHub/frontend/dashboard.html

257 lines
12 KiB
HTML
Raw Normal View History

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Dashboard ShooterHub</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
<link rel="stylesheet" href="/css/app.css">
<style>
.quick-link { display:flex; align-items:center; gap:.45rem; padding:.35rem .75rem;
border-radius:20px; font-size:.85rem; font-weight:500;
text-decoration:none; color:#495057; background:#f8f9fa;
border:1px solid #e9ecef; transition:background .15s,color .15s; white-space:nowrap; }
.quick-link:hover { background:#0d6efd; color:#fff; border-color:#0d6efd; }
.gear-card { text-decoration:none; color:inherit; }
.gear-card:hover .card { box-shadow:0 0 0 2px #0d6efd44 !important; }
.dash-box { min-height:260px; }
.dash-list-item { padding:.45rem .6rem; border-radius:5px; font-size:.85rem; }
.dash-list-item:hover { background:#f0f4ff; }
.dash-list-item + .dash-list-item { border-top:1px solid #f0f0f0; }
.photo-grid { display:grid; grid-template-columns:repeat(3,1fr); gap:6px; }
.photo-grid img { width:100%; aspect-ratio:1; object-fit:cover; border-radius:5px; cursor:pointer; }
.type-badge { font-size:.65rem; padding:.15em .45em; vertical-align:middle; }
</style>
</head>
<body>
<div id="navbar"></div>
<div class="container py-4">
<div class="mb-4">
<h2 class="fw-bold mb-0" id="greeting" data-i18n="dash.title">Dashboard</h2>
<p class="text-muted small mb-0" id="welcomeMsg" data-i18n="dash.welcome">Welcome back!</p>
</div>
<!-- ── Row 1: Compact quick links ─────────────────────────────────────── -->
<div class="d-flex flex-wrap gap-2 mb-4">
<a href="/gears.html" class="quick-link"><i class="bi bi-tools"></i><span data-i18n="dash.quicklink.gears">Gears &amp; rigs</span></a>
<a href="/reloads.html" class="quick-link"><i class="bi bi-gear-wide-connected"></i><span data-i18n="dash.quicklink.reloads">Reloading</span></a>
<a href="/sessions.html" class="quick-link"><i class="bi bi-activity"></i><span data-i18n="dash.quicklink.sessions">Sessions</span></a>
<a href="/chrono.html" class="quick-link"><i class="bi bi-speedometer2"></i><span data-i18n="dash.quicklink.chrono">Chronograph</span></a>
<a href="/photos.html" class="quick-link"><i class="bi bi-images"></i><span data-i18n="nav.photos">Photos</span></a>
<a href="/tools.html" class="quick-link"><i class="bi bi-wrench-adjustable"></i><span data-i18n="dash.quicklink.tools">Tools</span></a>
<a href="/profile.html" class="quick-link"><i class="bi bi-person-circle"></i><span data-i18n="dash.quicklink.profile">Profile</span></a>
</div>
<!-- ── Row 2: Gear stat buttons ───────────────────────────────────────── -->
<div class="row g-3 mb-4">
<div class="col-6 col-sm-3">
<a href="/gears.html" class="gear-card">
<div class="card border-0 shadow-sm h-100">
<div class="card-body d-flex align-items-center gap-3 py-3">
<div class="feature-icon text-primary"><i class="bi bi-archive"></i></div>
<div>
<div class="fs-3 fw-bold lh-1 mb-1" id="statGear"></div>
<div class="text-muted small" data-i18n="dash.stat.gear">Gear items</div>
</div>
</div>
</div>
</a>
</div>
<div class="col-6 col-sm-3">
<a href="/gears.html" class="gear-card">
<div class="card border-0 shadow-sm h-100">
<div class="card-body d-flex align-items-center gap-3 py-3">
<div class="feature-icon text-primary"><i class="bi bi-diagram-3"></i></div>
<div>
<div class="fs-3 fw-bold lh-1 mb-1" id="statRigs"></div>
<div class="text-muted small" data-i18n="dash.stat.rigs">Rigs</div>
</div>
</div>
</div>
</a>
</div>
<div class="col-6 col-sm-3">
<a href="/reloads.html" class="gear-card">
<div class="card border-0 shadow-sm h-100">
<div class="card-body d-flex align-items-center gap-3 py-3">
<div class="feature-icon text-primary"><i class="bi bi-gear-wide-connected"></i></div>
<div>
<div class="fs-3 fw-bold lh-1 mb-1" id="statRecipes"></div>
<div class="text-muted small" data-i18n="dash.stat.recipes">Load recipes</div>
</div>
</div>
</div>
</a>
</div>
<div class="col-6 col-sm-3">
<a href="/reloads.html" class="gear-card">
<div class="card border-0 shadow-sm h-100">
<div class="card-body d-flex align-items-center gap-3 py-3">
<div class="feature-icon text-primary"><i class="bi bi-stack"></i></div>
<div>
<div class="fs-3 fw-bold lh-1 mb-1" id="statBatches"></div>
<div class="text-muted small" data-i18n="dash.stat.batches">Ammo batches</div>
</div>
</div>
</div>
</a>
</div>
</div>
<!-- ── Row 3: Live content boxes ──────────────────────────────────────── -->
<div class="row g-3">
<!-- Sessions -->
<div class="col-md-4">
<div class="card border-0 shadow-sm dash-box">
<div class="card-body">
<div class="d-flex align-items-center justify-content-between mb-3">
<h6 class="fw-semibold mb-0"><i class="bi bi-activity me-2 text-primary"></i>Sessions</h6>
<a href="/sessions.html" class="small text-primary text-decoration-none">View all →</a>
</div>
<div id="dashSessions">
<div class="text-center py-4"><div class="spinner-border spinner-border-sm text-primary"></div></div>
</div>
</div>
</div>
</div>
<!-- Analyses -->
<div class="col-md-4">
<div class="card border-0 shadow-sm dash-box">
<div class="card-body">
<div class="d-flex align-items-center justify-content-between mb-3">
<h6 class="fw-semibold mb-0"><i class="bi bi-speedometer2 me-2 text-primary"></i>Analyses</h6>
<a href="/chrono.html" class="small text-primary text-decoration-none">View all →</a>
</div>
<div id="dashAnalyses">
<div class="text-center py-4"><div class="spinner-border spinner-border-sm text-primary"></div></div>
</div>
</div>
</div>
</div>
<!-- Photos -->
<div class="col-md-4">
<div class="card border-0 shadow-sm dash-box">
<div class="card-body">
<div class="d-flex align-items-center justify-content-between mb-3">
<h6 class="fw-semibold mb-0"><i class="bi bi-images me-2 text-primary"></i>Recent photos</h6>
<a href="/photos.html" class="small text-primary text-decoration-none">View all →</a>
</div>
<div id="dashPhotos">
<div class="text-center py-4"><div class="spinner-border spinner-border-sm text-primary"></div></div>
</div>
</div>
</div>
</div>
</div>
</div>
<div id="toastContainer"></div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
<script src="/js/api.js"></script>
<script src="/js/utils.js"></script>
<script src="/js/i18n.js"></script>
<script src="/js/nav.js"></script>
<script>
function esc(s) { const d = document.createElement('div'); d.textContent = s; return d.innerHTML; }
async function loadDashboard() {
try {
const [profile, inventory, rigs, recipes] = await Promise.all([
apiGet('/users/profile/'),
apiGet('/inventory/'),
apiGet('/rigs/'),
apiGet('/reloading/recipes/'),
]);
const name = profile.first_name || profile.username || '';
if (name) document.getElementById('welcomeMsg').textContent =
(t('dash.welcome.name') || 'Welcome back, {name}!').replace('{name}', name);
const inv = Array.isArray(inventory) ? inventory : (inventory.results || []);
const rig = Array.isArray(rigs) ? rigs : (rigs.results || []);
const rec = Array.isArray(recipes) ? recipes : (recipes.results || []);
document.getElementById('statGear').textContent = inv.length;
document.getElementById('statRigs').textContent = rig.length;
document.getElementById('statRecipes').textContent = rec.length;
document.getElementById('statBatches').textContent = rec.reduce((n, r) => n + (r.batches?.length || 0), 0);
} catch(e) { console.error(e); }
loadSessions();
loadAnalyses();
loadPhotos();
}
async function loadSessions() {
const el = document.getElementById('dashSessions');
try {
const [prs, fp, ss] = await Promise.all([
apiGet('/sessions/prs/?page_size=5').then(d => (Array.isArray(d) ? d : (d.results||[])).map(s => ({...s, _type:'PRS'}))).catch(()=>[]),
apiGet('/sessions/free-practice/?page_size=5').then(d => (Array.isArray(d) ? d : (d.results||[])).map(s => ({...s, _type:'Practice'}))).catch(()=>[]),
apiGet('/sessions/speed-shooting/?page_size=5').then(d => (Array.isArray(d) ? d : (d.results||[])).map(s => ({...s, _type:'Speed'}))).catch(()=>[]),
]);
const all = [...prs, ...fp, ...ss]
.sort((a, b) => (b.date || '').localeCompare(a.date || ''))
.slice(0, 7);
if (!all.length) { el.innerHTML = '<p class="text-muted small">No sessions yet.</p>'; return; }
const typeColor = { PRS:'primary', Practice:'success', Speed:'warning' };
el.innerHTML = all.map(s => {
const label = s.competition_name || s.name || '(unnamed)';
const color = typeColor[s._type] || 'secondary';
return `<div class="dash-list-item d-flex align-items-center gap-2">
<span class="badge bg-${color} type-badge">${s._type}</span>
<span class="flex-grow-1 text-truncate">${esc(label)}</span>
<span class="text-muted small">${s.date || ''}</span>
</div>`;
}).join('');
} catch(e) {
el.innerHTML = '<p class="text-danger small">Failed to load.</p>';
}
}
async function loadAnalyses() {
const el = document.getElementById('dashAnalyses');
try {
const data = await apiGet('/tools/chronograph/?page_size=7');
const items = Array.isArray(data) ? data : (data.results || []);
if (!items.length) { el.innerHTML = '<p class="text-muted small">No analyses yet.</p>'; return; }
el.innerHTML = items.map(a => `
<a href="/chrono.html?id=${a.id}" class="dash-list-item d-flex align-items-center gap-2 text-decoration-none text-reset">
<i class="bi bi-speedometer2 text-muted small"></i>
<span class="flex-grow-1 text-truncate">${esc(a.name || '(unnamed)')}</span>
<span class="text-muted small">${a.date || ''}</span>
</a>`).join('');
} catch(e) {
el.innerHTML = '<p class="text-danger small">Failed to load.</p>';
}
}
async function loadPhotos() {
const el = document.getElementById('dashPhotos');
try {
const data = await apiGet('/photos/group-photos/?page_size=9');
const items = Array.isArray(data) ? data : (data.results || []);
if (!items.length) { el.innerHTML = '<p class="text-muted small">No photos yet.</p>'; return; }
el.innerHTML = `<div class="photo-grid">${
items.map(gp => `<img src="/api/photos/${gp.photo.id}/data/"
onclick="window.open('/api/photos/${gp.photo.id}/data/','_blank')"
title="${esc(gp.caption || '')}">`).join('')
}</div>`;
} catch(e) {
el.innerHTML = '<p class="text-danger small">Failed to load.</p>';
}
}
loadDashboard();
</script>
</body>
</html>