Files
ShooterHub/frontend/dashboard.html
2026-04-02 11:24:30 +02:00

257 lines
12 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!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>