270 lines
12 KiB
HTML
270 lines
12 KiB
HTML
<!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 & 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">
|
||
© 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>
|