First commit of claude's rework in django + vanillajs fronted
This commit is contained in:
256
frontend/dashboard.html
Normal file
256
frontend/dashboard.html
Normal file
@@ -0,0 +1,256 @@
|
||||
<!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 & 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>
|
||||
Reference in New Issue
Block a user