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

270 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>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>