Files
ShooterHub/frontend/index.html

270 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>ShooterHub Manage, Track, Share</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>
.feed-card { min-height: 280px; }
.feed-item { padding: .4rem .5rem; border-radius: 5px; font-size: .84rem; }
.feed-item:hover { background: #f0f4ff; }
.feed-item + .feed-item { border-top: 1px solid #f0f0f0; }
.feed-photo-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 5px; }
.feed-photo-grid img { width: 100%; aspect-ratio: 1; object-fit: cover; border-radius: 4px; cursor: pointer; }
.type-badge { font-size: .62rem; padding: .1em .4em; vertical-align: middle; }
.empty-feed { color: #adb5bd; font-size: .85rem; padding: 2rem 0; }
</style>
</head>
<body>
<div id="navbar"></div>
<!-- Hero -->
<section class="hero">
<div class="container text-center">
<div class="mb-3">
<i class="bi bi-crosshair2" style="font-size:4rem; color:#0d6efd;"></i>
</div>
<h1>ShooterHub</h1>
<p class="lead mx-auto" style="max-width:600px;" data-i18n="index.lead">
Your all-in-one platform to manage your firearms &amp; gear, log and analyse
your shooting performance, develop custom reloads, and share your results
with other shooters.
</p>
<div class="mt-4 d-flex gap-3 justify-content-center flex-wrap">
<a href="/register.html" class="btn btn-primary btn-lg px-5" data-i18n="index.cta">Get started free</a>
<a href="/tools.html" class="btn btn-outline-light btn-lg px-5" data-i18n="index.cta2">Explore tools</a>
</div>
</div>
</section>
<!-- Features -->
<section class="py-5">
<div class="container">
<div class="text-center mb-5">
<h2 class="fw-bold" data-i18n="index.feat.title">Everything a serious shooter needs</h2>
<p class="text-muted" data-i18n="index.feat.sub">From the first round to competition-level analysis.</p>
</div>
<div class="row g-4">
<div class="col-md-6 col-lg-3">
<div class="card feature-card h-100 p-4">
<div class="feature-icon mb-3"><i class="bi bi-archive"></i></div>
<h5 class="fw-semibold" data-i18n="index.feat.gear.title">Gear Inventory</h5>
<p class="text-muted small mb-0" data-i18n="index.feat.gear.desc">
Catalogue every firearm, scope, suppressor, bipod and magazine you own.
Build custom rigs and share them publicly.
</p>
</div>
</div>
<div class="col-md-6 col-lg-3">
<div class="card feature-card h-100 p-4">
<div class="feature-icon mb-3"><i class="bi bi-activity"></i></div>
<h5 class="fw-semibold" data-i18n="index.feat.sessions.title">Session Logging</h5>
<p class="text-muted small mb-0" data-i18n="index.feat.sessions.desc">
Log every shooting session with chrono data, shot groups, weather
conditions and notes in one place.
</p>
</div>
</div>
<div class="col-md-6 col-lg-3">
<div class="card feature-card h-100 p-4">
<div class="feature-icon mb-3"><i class="bi bi-bar-chart-line"></i></div>
<h5 class="fw-semibold" data-i18n="index.feat.analysis.title">Performance Analysis</h5>
<p class="text-muted small mb-0" data-i18n="index.feat.analysis.desc">
Visualise velocity SD, ES, group sizes over time and identify trends
in your shooting performance.
</p>
</div>
</div>
<div class="col-md-6 col-lg-3">
<div class="card feature-card h-100 p-4">
<div class="feature-icon mb-3"><i class="bi bi-gear-wide-connected"></i></div>
<h5 class="fw-semibold" data-i18n="index.feat.reload.title">Reload Development</h5>
<p class="text-muted small mb-0" data-i18n="index.feat.reload.desc">
Build load recipes, vary powder charge across batches, link each batch
to shot groups and find the most accurate charge.
</p>
</div>
</div>
</div>
</div>
</section>
<!-- Community Feed -->
<section class="py-5 bg-light">
<div class="container">
<div class="mb-4">
<h4 class="fw-bold mb-1"><i class="bi bi-globe2 me-2 text-primary"></i>Community Feed</h4>
<p class="text-muted small mb-0">Publicly shared sessions, analyses, photos and reload recipes from the community.</p>
</div>
<div class="row g-3">
<!-- Sessions -->
<div class="col-md-6 col-xl-3">
<div class="card border-0 shadow-sm feed-card">
<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="feedSessions">
<div class="text-center py-3"><div class="spinner-border spinner-border-sm text-primary"></div></div>
</div>
</div>
</div>
</div>
<!-- Analyses -->
<div class="col-md-6 col-xl-3">
<div class="card border-0 shadow-sm feed-card">
<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="feedAnalyses">
<div class="text-center py-3"><div class="spinner-border spinner-border-sm text-primary"></div></div>
</div>
</div>
</div>
</div>
<!-- Photos -->
<div class="col-md-6 col-xl-3">
<div class="card border-0 shadow-sm feed-card">
<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>Photos</h6>
<a href="/photos.html" class="small text-primary text-decoration-none">View all →</a>
</div>
<div id="feedPhotos">
<div class="text-center py-3"><div class="spinner-border spinner-border-sm text-primary"></div></div>
</div>
</div>
</div>
</div>
<!-- Reload Recipes -->
<div class="col-md-6 col-xl-3">
<div class="card border-0 shadow-sm feed-card">
<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-gear-wide-connected me-2 text-primary"></i>Load recipes</h6>
<a href="/reloads.html" class="small text-primary text-decoration-none">View all →</a>
</div>
<div id="feedRecipes">
<div class="text-center py-3"><div class="spinner-border spinner-border-sm text-primary"></div></div>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
<!-- CTA -->
<section class="bg-dark text-white py-5">
<div class="container text-center">
<h3 class="fw-bold mb-3" data-i18n="index.cta3">Ready to elevate your shooting?</h3>
<a href="/register.html" class="btn btn-primary btn-lg px-5" data-i18n="index.cta4">Create your account</a>
</div>
</section>
<footer class="text-center text-muted py-4 small">
&copy; 2026 ShooterHub
</footer>
<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>
const TYPE_COLOR = { PRS: 'primary', Practice: 'success', Speed: 'warning' };
async function loadFeed() {
try {
const data = await apiGet('/feed/');
// ── Sessions ────────────────────────────────────────────────────────────
const sEl = document.getElementById('feedSessions');
if (!data.sessions.length) {
sEl.innerHTML = '<p class="empty-feed text-center">No public sessions yet.</p>';
} else {
sEl.innerHTML = data.sessions.map(s => {
const color = TYPE_COLOR[s.type] || 'secondary';
return `<div class="feed-item d-flex align-items-center gap-2">
<span class="badge bg-${color} type-badge">${esc(s.type)}</span>
<span class="flex-grow-1 text-truncate">${esc(s.label)}</span>
<span class="text-muted" style="font-size:.75rem;white-space:nowrap">${s.date || ''}</span>
</div>`;
}).join('');
}
// ── Analyses ────────────────────────────────────────────────────────────
const aEl = document.getElementById('feedAnalyses');
if (!data.analyses.length) {
aEl.innerHTML = '<p class="empty-feed text-center">No public analyses yet.</p>';
} else {
aEl.innerHTML = data.analyses.map(a =>
`<a href="/chrono.html?id=${a.id}" class="feed-item d-flex align-items-center gap-2 text-decoration-none text-reset">
<i class="bi bi-speedometer2 text-muted" style="font-size:.8rem"></i>
<span class="flex-grow-1 text-truncate">${esc(a.name)}</span>
<span class="text-muted" style="font-size:.75rem;white-space:nowrap">${a.date || ''}</span>
</a>`
).join('');
}
// ── Photos ──────────────────────────────────────────────────────────────
const pEl = document.getElementById('feedPhotos');
if (!data.photos.length) {
pEl.innerHTML = '<p class="empty-feed text-center">No public photos yet.</p>';
} else {
const items = data.photos.slice(0, 9);
pEl.innerHTML = `<div class="feed-photo-grid">${
items.map(gp => {
const title = gp.caption || (gp.group_size_mm ? `ES ${parseFloat(gp.group_size_mm).toFixed(1)} mm` : '');
return `<img src="/api/photos/${gp.photo_id}/data/"
title="${esc(title)}"
onclick="window.open('/api/photos/${gp.photo_id}/data/','_blank')"
loading="lazy">`;
}).join('')
}</div>`;
}
// ── Recipes ─────────────────────────────────────────────────────────────
const rEl = document.getElementById('feedRecipes');
if (!data.recipes.length) {
rEl.innerHTML = '<p class="empty-feed text-center">No public recipes yet.</p>';
} else {
rEl.innerHTML = data.recipes.map(r =>
`<div class="feed-item">
<div class="fw-semibold text-truncate">${esc(r.name)}</div>
<div class="text-muted" style="font-size:.75rem">${[r.caliber, r.bullet].filter(Boolean).join(' · ')}</div>
</div>`
).join('');
}
} catch(e) {
['feedSessions','feedAnalyses','feedPhotos','feedRecipes'].forEach(id => {
const el = document.getElementById(id);
if (el) el.innerHTML = '<p class="text-muted small text-center py-3">No data available.</p>';
});
}
}
loadFeed();
</script>
</body>
</html>