Vibe coded a bit more ... now we have session, attached picture and analysis, MOA group computation

This commit is contained in:
Gérald Colangelo
2026-03-17 17:20:54 +01:00
parent 120dc70cf5
commit 5b18fadb60
55 changed files with 5419 additions and 59 deletions

157
templates/index.html Normal file
View File

@@ -0,0 +1,157 @@
{% extends "base.html" %}
{% block title %}The Shooter's Network — Track, analyze, share{% endblock %}
{% block body %}
<nav class="nav">
<a href="/" class="nav-brand">The Shooter's Network</a>
<div class="nav-links">
{% if current_user.is_authenticated %}
<a href="{{ url_for('analyze') }}">New Analysis</a>
<a href="{{ url_for('equipment.index') }}">Equipment</a>
<a href="{{ url_for('sessions.index') }}">Sessions</a>
<a href="{{ url_for('dashboard.index') }}">Dashboard</a>
{% endif %}
</div>
<div class="nav-right">
{% if current_user.is_authenticated %}
<div class="nav-dropdown" id="userDropdown">
<button class="nav-user-btn" onclick="toggleDropdown(event)">
{% set av = current_user.effective_avatar_url %}
{% if av %}<img src="{{ av }}" class="nav-avatar" alt="">
{% else %}<span style="font-size:1.1rem;line-height:1;">&#128100;</span>{% endif %}
<span>{{ current_user.display_name or current_user.email.split('@')[0] }}</span>
<span class="caret">&#9660;</span>
</button>
<div class="nav-dd-menu">
<a href="{{ url_for('auth.profile') }}">&#128100;&ensp;Profile</a>
<hr>
<form method="post" action="{{ url_for('auth.logout') }}">
<button type="submit">&#8594;&ensp;Logout</button>
</form>
</div>
</div>
{% else %}
<a href="{{ url_for('auth.login') }}" style="color:#c8cfe0;font-size:0.9rem;text-decoration:none;">Login</a>
<a href="{{ url_for('auth.register') }}"
style="background:#1f77b4;color:#fff;padding:0.35rem 0.9rem;border-radius:4px;font-size:0.88rem;text-decoration:none;">
Join free
</a>
{% endif %}
</div>
</nav>
<script>
function toggleDropdown(e) {
e.stopPropagation();
document.getElementById('userDropdown').classList.toggle('open');
}
document.addEventListener('click', function() {
var d = document.getElementById('userDropdown');
if (d) d.classList.remove('open');
});
</script>
<!-- ── Hero ── -->
<section style="background:#1a1a2e;color:#fff;padding:4rem 1.5rem;text-align:center;">
<h1 style="font-size:2.4rem;font-weight:800;color:#fff;margin-bottom:0.75rem;letter-spacing:-0.02em;">
The Shooter's Network
</h1>
<p style="font-size:1.15rem;color:#a0aec0;max-width:560px;margin:0 auto 2rem;">
Analyze your ballistic data, track every session, manage your equipment,
and share your performance with the community.
</p>
<div style="display:flex;gap:1rem;justify-content:center;flex-wrap:wrap;">
{% if current_user.is_authenticated %}
<a href="{{ url_for('analyze') }}"
style="background:#1f77b4;color:#fff;padding:0.75rem 1.75rem;border-radius:6px;font-size:1rem;font-weight:600;text-decoration:none;">
New Analysis
</a>
<a href="{{ url_for('sessions.new') }}"
style="background:transparent;color:#fff;padding:0.75rem 1.75rem;border-radius:6px;font-size:1rem;font-weight:600;text-decoration:none;border:1px solid #4a5568;">
Log a Session
</a>
{% else %}
<a href="{{ url_for('auth.register') }}"
style="background:#1f77b4;color:#fff;padding:0.75rem 1.75rem;border-radius:6px;font-size:1rem;font-weight:600;text-decoration:none;">
Get started — free
</a>
<a href="{{ url_for('analyze') }}"
style="background:transparent;color:#fff;padding:0.75rem 1.75rem;border-radius:6px;font-size:1rem;font-weight:600;text-decoration:none;border:1px solid #4a5568;">
Try without account
</a>
{% endif %}
</div>
</section>
<!-- ── Features ── -->
<section style="background:#f8f9fb;padding:2.5rem 1.5rem;">
<div style="max-width:900px;margin:0 auto;display:grid;grid-template-columns:repeat(auto-fit,minmax(230px,1fr));gap:1.25rem;">
<div style="background:#fff;padding:1.5rem;border-radius:8px;box-shadow:0 1px 4px rgba(0,0,0,.07);">
<div style="font-size:1.6rem;margin-bottom:0.5rem;">📊</div>
<h3 style="margin:0 0 .35rem;color:#1a1a2e;font-size:1rem;">Ballistic Analysis</h3>
<p style="color:#666;font-size:0.88rem;margin:0;">Upload CSV files from your chronograph and get instant shot-group statistics, velocity charts, and PDF reports.</p>
</div>
<div style="background:#fff;padding:1.5rem;border-radius:8px;box-shadow:0 1px 4px rgba(0,0,0,.07);">
<div style="font-size:1.6rem;margin-bottom:0.5rem;">🎯</div>
<h3 style="margin:0 0 .35rem;color:#1a1a2e;font-size:1rem;">Session Tracking</h3>
<p style="color:#666;font-size:0.88rem;margin:0;">Log every range visit with location, weather, rifle, ammo, and distance. All your data in one place.</p>
</div>
<div style="background:#fff;padding:1.5rem;border-radius:8px;box-shadow:0 1px 4px rgba(0,0,0,.07);">
<div style="font-size:1.6rem;margin-bottom:0.5rem;">🤝</div>
<h3 style="margin:0 0 .35rem;color:#1a1a2e;font-size:1rem;">Community Feed</h3>
<p style="color:#666;font-size:0.88rem;margin:0;">Share your public sessions and see what other shooters are achieving on the range.</p>
</div>
</div>
</section>
<!-- ── Flash messages ── -->
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
<div style="max-width:960px;margin:1rem auto;padding:0 1.5rem;">
{% for category, message in messages %}
<div class="flash {{ category }}">{{ message }}</div>
{% endfor %}
</div>
{% endif %}
{% endwith %}
<!-- ── Public sessions feed ── -->
<section style="padding:2.5rem 1.5rem 3rem;">
<div style="max-width:960px;margin:0 auto;">
<h2 style="font-size:1.3rem;color:#1a1a2e;margin-bottom:1.25rem;border-bottom:2px solid #e0e0e0;padding-bottom:.4rem;">
Latest sessions
</h2>
{% if public_sessions %}
<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:1.1rem;">
{% for s in public_sessions %}
<a href="{{ url_for('sessions.detail', session_id=s.id) }}"
style="display:block;background:#fff;border:1px solid #e8e8e8;border-radius:8px;padding:1.1rem 1.25rem;text-decoration:none;color:inherit;">
<div style="display:flex;align-items:center;gap:0.55rem;margin-bottom:0.65rem;">
{% if s.user.avatar_url %}
<img src="{{ s.user.avatar_url }}" style="width:26px;height:26px;border-radius:50%;object-fit:cover;" alt="">
{% else %}
<div style="width:26px;height:26px;border-radius:50%;background:#e0e4f0;display:flex;align-items:center;justify-content:center;font-size:0.72rem;color:#666;font-weight:700;">
{{ (s.user.display_name or s.user.email)[0].upper() }}
</div>
{% endif %}
<span style="font-size:0.83rem;color:#666;">{{ s.user.display_name or s.user.email.split('@')[0] }}</span>
</div>
<div style="font-weight:600;color:#1a1a2e;margin-bottom:0.35rem;font-size:0.95rem;">{{ s.label }}</div>
<div style="font-size:0.81rem;color:#888;display:flex;flex-wrap:wrap;gap:.3rem .65rem;">
<span>{{ s.session_date.strftime('%d %b %Y') }}</span>
{% if s.location_name %}<span>📍 {{ s.location_name }}</span>{% endif %}
{% if s.distance_m %}<span>{{ s.distance_m }} m</span>{% endif %}
{% if s.weather_cond %}<span>{{ s.weather_cond.replace('_', ' ').title() }}</span>{% endif %}
{% if s.weather_temp_c is not none %}<span>{{ s.weather_temp_c }}°C</span>{% endif %}
</div>
</a>
{% endfor %}
</div>
{% else %}
<p style="color:#aaa;text-align:center;padding:3rem 0;">
No public sessions yet. Be the first to share one!
</p>
{% endif %}
</div>
</section>
{% endblock %}