WIP: claude works hard
This commit is contained in:
@@ -6,51 +6,51 @@
|
||||
<div>
|
||||
<div style="font-size:0.82rem;color:#888;margin-bottom:.3rem;">
|
||||
{% if analysis.session_id %}
|
||||
<a href="{{ url_for('sessions.detail', session_id=analysis.session_id) }}">Session</a> ›
|
||||
<a href="{{ url_for('sessions.detail', session_id=analysis.session_id) }}">{{ _('Session') }}</a> ›
|
||||
{% else %}
|
||||
<a href="{{ url_for('dashboard.index') }}">Dashboard</a> ›
|
||||
<a href="{{ url_for('dashboard.index') }}">{{ _('Dashboard') }}</a> ›
|
||||
{% endif %}
|
||||
Analysis
|
||||
{{ _('Analysis') }}
|
||||
</div>
|
||||
<h1 style="margin:0;">{{ analysis.title }}</h1>
|
||||
<div style="font-size:0.85rem;color:#888;margin-top:.3rem;">
|
||||
{{ analysis.created_at.strftime('%d %b %Y') }}
|
||||
· {{ analysis.shot_count }} shot(s)
|
||||
· {{ analysis.group_count }} group(s)
|
||||
· {{ analysis.shot_count }} {{ _('shot(s)') }}
|
||||
· {{ analysis.group_count }} {{ _('group(s)') }}
|
||||
</div>
|
||||
</div>
|
||||
<div style="display:flex;gap:.75rem;align-items:center;flex-wrap:wrap;">
|
||||
{% if has_pdf %}
|
||||
<a href="{{ url_for('analyses.download_pdf', analysis_id=analysis.id) }}"
|
||||
style="background:#1f77b4;color:#fff;border-radius:4px;padding:0.5rem 1.1rem;font-size:0.9rem;text-decoration:none;">
|
||||
⇓ Download PDF report
|
||||
⇓ {{ _('Download PDF report') }}
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if current_user.is_authenticated and current_user.id == analysis.user_id %}
|
||||
<form method="post" action="{{ url_for('analyses.delete', analysis_id=analysis.id) }}"
|
||||
onsubmit="return confirm('Delete this analysis? The CSV and PDF will be permanently removed.');">
|
||||
onsubmit="return confirm('{{ _('Delete this analysis? The CSV and PDF will be permanently removed.') | e }}');">
|
||||
<button type="submit"
|
||||
style="background:#fff0f0;color:#c0392b;border:1px solid #f5c6c6;border-radius:4px;padding:0.5rem 1.1rem;font-size:0.9rem;cursor:pointer;">
|
||||
Delete
|
||||
{{ _('Delete') }}
|
||||
</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
<a href="{{ url_for('analyze') }}" style="font-size:0.9rem;color:#666;">← New analysis</a>
|
||||
<a href="{{ url_for('analyze') }}" style="font-size:0.9rem;color:#666;">← {{ _('New analysis') }}</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2>Overall Statistics</h2>
|
||||
<h2>{{ _('Overall Statistics') }}</h2>
|
||||
<table style="max-width:480px;">
|
||||
<thead>
|
||||
<tr><th>Metric</th><th>Value</th></tr>
|
||||
<tr><th>{{ _('Metric') }}</th><th>{{ _('Value') }}</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr><td>Total shots</td><td>{{ overall.count }}</td></tr>
|
||||
<tr><td>Min speed</td><td>{{ "%.4f"|format(overall.min_speed) }}</td></tr>
|
||||
<tr><td>Max speed</td><td>{{ "%.4f"|format(overall.max_speed) }}</td></tr>
|
||||
<tr><td>Mean speed</td><td>{{ "%.4f"|format(overall.mean_speed) }}</td></tr>
|
||||
<tr><td>{{ _('Total shots') }}</td><td>{{ overall.count }}</td></tr>
|
||||
<tr><td>{{ _('Min speed') }}</td><td>{{ "%.4f"|format(overall.min_speed) }}</td></tr>
|
||||
<tr><td>{{ _('Max speed') }}</td><td>{{ "%.4f"|format(overall.max_speed) }}</td></tr>
|
||||
<tr><td>{{ _('Mean speed') }}</td><td>{{ "%.4f"|format(overall.mean_speed) }}</td></tr>
|
||||
<tr>
|
||||
<td>Std dev (speed)</td>
|
||||
<td>{{ _('Std dev (speed)') }}</td>
|
||||
<td>
|
||||
{% if overall.std_speed is not none %}{{ "%.4f"|format(overall.std_speed) }}{% else %}–{% endif %}
|
||||
</td>
|
||||
@@ -61,24 +61,24 @@
|
||||
<img class="chart-img" src="data:image/png;base64,{{ overview_chart }}"
|
||||
alt="Avg speed and std dev per group" style="max-width:600px;margin:1rem 0 1.5rem;">
|
||||
|
||||
<h2>Groups — {{ groups_display|length }} group(s) detected</h2>
|
||||
<h2>{{ _('Groups') }} — {{ groups_display|length }} {{ _('group(s) detected') }}</h2>
|
||||
|
||||
{% for stat, chart_b64 in groups_display %}
|
||||
<div class="group-section">
|
||||
<h3>Group {{ stat.group_index }}</h3>
|
||||
<h3>{{ _('Group %(n)s', n=stat.group_index) }}</h3>
|
||||
<div class="group-meta">
|
||||
{{ stat.time_start }} – {{ stat.time_end }} | {{ stat.count }} shot(s)
|
||||
{{ stat.time_start }} – {{ stat.time_end }} | {{ stat.count }} {{ _('shot(s)') }}
|
||||
</div>
|
||||
<table style="max-width:480px;">
|
||||
<thead>
|
||||
<tr><th>Metric</th><th>Value</th></tr>
|
||||
<tr><th>{{ _('Metric') }}</th><th>{{ _('Value') }}</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr><td>Min speed</td><td>{{ "%.4f"|format(stat.min_speed) }}</td></tr>
|
||||
<tr><td>Max speed</td><td>{{ "%.4f"|format(stat.max_speed) }}</td></tr>
|
||||
<tr><td>Mean speed</td><td>{{ "%.4f"|format(stat.mean_speed) }}</td></tr>
|
||||
<tr><td>{{ _('Min speed') }}</td><td>{{ "%.4f"|format(stat.min_speed) }}</td></tr>
|
||||
<tr><td>{{ _('Max speed') }}</td><td>{{ "%.4f"|format(stat.max_speed) }}</td></tr>
|
||||
<tr><td>{{ _('Mean speed') }}</td><td>{{ "%.4f"|format(stat.mean_speed) }}</td></tr>
|
||||
<tr>
|
||||
<td>Std dev (speed)</td>
|
||||
<td>{{ _('Std dev (speed)') }}</td>
|
||||
<td>
|
||||
{% if stat.std_speed is not none %}{{ "%.4f"|format(stat.std_speed) }}{% else %}–{% endif %}
|
||||
</td>
|
||||
|
||||
@@ -1,22 +1,22 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Login — Ballistic Analyzer{% endblock %}
|
||||
{% block title %}{{ _('Sign in') }} — Ballistic Analyzer{% endblock %}
|
||||
{% block content %}
|
||||
<h1>Sign in</h1>
|
||||
<h1>{{ _('Sign in') }}</h1>
|
||||
|
||||
<form method="post" action="{{ url_for('auth.login') }}" style="max-width:360px;margin-bottom:1.5rem;">
|
||||
<div style="margin-bottom:1rem;">
|
||||
<label style="display:block;font-size:0.88rem;font-weight:600;color:#444;margin-bottom:0.3rem;">Email</label>
|
||||
<label style="display:block;font-size:0.88rem;font-weight:600;color:#444;margin-bottom:0.3rem;">{{ _('Email') }}</label>
|
||||
<input type="email" name="email" value="{{ prefill_email or '' }}" required autocomplete="email"
|
||||
style="width:100%;padding:0.55rem 0.75rem;border:1px solid #ccc;border-radius:4px;font-size:0.95rem;">
|
||||
</div>
|
||||
<div style="margin-bottom:1.25rem;">
|
||||
<label style="display:block;font-size:0.88rem;font-weight:600;color:#444;margin-bottom:0.3rem;">Password</label>
|
||||
<label style="display:block;font-size:0.88rem;font-weight:600;color:#444;margin-bottom:0.3rem;">{{ _('Password') }}</label>
|
||||
<input type="password" name="password" required autocomplete="current-password"
|
||||
style="width:100%;padding:0.55rem 0.75rem;border:1px solid #ccc;border-radius:4px;font-size:0.95rem;">
|
||||
</div>
|
||||
<button type="submit"
|
||||
style="width:100%;background:#1a1a2e;color:#fff;border:none;border-radius:4px;padding:0.65rem;font-size:0.95rem;cursor:pointer;">
|
||||
Sign in
|
||||
{{ _('Sign in') }}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
@@ -24,18 +24,18 @@
|
||||
<form method="post" action="{{ url_for('auth.resend_confirmation') }}" style="margin-bottom:1.5rem;">
|
||||
<input type="hidden" name="email" value="{{ resend_email }}">
|
||||
<button type="submit" class="btn-link" style="color:#1f77b4;font-size:0.88rem;">
|
||||
Resend confirmation email
|
||||
{{ _('Resend confirmation email') }}
|
||||
</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
|
||||
<p style="font-size:0.9rem;color:#555;margin-bottom:1.5rem;">
|
||||
Don't have an account? <a href="{{ url_for('auth.register') }}">Create one</a>
|
||||
{{ _("Don't have an account?") }} <a href="{{ url_for('auth.register') }}">{{ _('Create one') }}</a>
|
||||
</p>
|
||||
|
||||
<div style="display:flex;align-items:center;gap:0.75rem;margin-bottom:1.5rem;max-width:360px;">
|
||||
<hr style="flex:1;border:none;border-top:1px solid #ddd;">
|
||||
<span style="font-size:0.8rem;color:#999;">or continue with</span>
|
||||
<span style="font-size:0.8rem;color:#999;">{{ _('or continue with') }}</span>
|
||||
<hr style="flex:1;border:none;border-top:1px solid #ddd;">
|
||||
</div>
|
||||
|
||||
@@ -48,7 +48,7 @@
|
||||
<path d="M3.964 10.71A5.41 5.41 0 0 1 3.682 9c0-.593.102-1.17.282-1.71V4.958H.957A8.996 8.996 0 0 0 0 9c0 1.452.348 2.827.957 4.042l3.007-2.332z" fill="#FBBC05"/>
|
||||
<path d="M9 3.58c1.321 0 2.508.454 3.44 1.345l2.582-2.58C13.463.891 11.426 0 9 0A8.997 8.997 0 0 0 .957 4.958L3.964 7.29C4.672 5.163 6.656 3.58 9 3.58z" fill="#EA4335"/>
|
||||
</svg>
|
||||
Continue with Google
|
||||
{{ _('Continue with Google') }}
|
||||
</a>
|
||||
|
||||
<a href="{{ url_for('auth.login_github') }}"
|
||||
@@ -56,7 +56,7 @@
|
||||
<svg width="17" height="17" viewBox="0 0 98 96" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M48.854 0C21.839 0 0 22 0 49.217c0 21.756 13.993 40.172 33.405 46.69 2.427.49 3.316-1.059 3.316-2.362 0-1.141-.08-5.052-.08-9.127-13.59 2.934-16.42-5.867-16.42-5.867-2.184-5.704-5.42-7.17-5.42-7.17-4.448-3.015.324-3.015.324-3.015 4.934.326 7.523 5.052 7.523 5.052 4.367 7.496 11.404 5.378 14.235 4.074.404-3.178 1.699-5.378 3.074-6.6-10.839-1.141-22.243-5.378-22.243-24.283 0-5.378 1.94-9.778 5.014-13.2-.485-1.222-2.184-6.275.486-13.038 0 0 4.125-1.304 13.426 5.052a46.97 46.97 0 0 1 12.214-1.63c4.125 0 8.33.571 12.213 1.63 9.302-6.356 13.427-5.052 13.427-5.052 2.67 6.763.97 11.816.485 13.038 3.155 3.422 5.015 7.822 5.015 13.2 0 18.905-11.404 23.06-22.324 24.283 1.78 1.548 3.316 4.481 3.316 9.126 0 6.6-.08 11.897-.08 13.526 0 1.304.89 2.853 3.316 2.364 19.412-6.52 33.405-24.935 33.405-46.691C97.707 22 75.788 0 48.854 0z" fill="#24292f"/>
|
||||
</svg>
|
||||
Continue with GitHub
|
||||
{{ _('Continue with GitHub') }}
|
||||
</a>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Profile — The Shooter's Network{% endblock %}
|
||||
{% block title %}{{ _('Profile') }} — The Shooter's Network{% endblock %}
|
||||
{% block content %}
|
||||
<h1>Profile</h1>
|
||||
<h1>{{ _('Profile') }}</h1>
|
||||
|
||||
{# ---- Avatar + display name ---- #}
|
||||
<h2>Account</h2>
|
||||
<h2>{{ _('Account') }}</h2>
|
||||
<form method="post" action="{{ url_for('auth.profile') }}"
|
||||
enctype="multipart/form-data"
|
||||
style="max-width:480px;">
|
||||
@@ -22,15 +22,15 @@
|
||||
{% endif %}
|
||||
<div>
|
||||
<label style="display:block;font-size:.85rem;font-weight:600;color:#444;margin-bottom:.3rem;">
|
||||
Profile picture
|
||||
{{ _('Profile picture') }}
|
||||
</label>
|
||||
<input type="file" name="avatar" accept="image/*" style="font-size:0.9rem;">
|
||||
<div style="font-size:0.78rem;color:#aaa;margin-top:.2rem;">JPEG/PNG, max 1200 px, auto-resized.</div>
|
||||
<div style="font-size:0.78rem;color:#aaa;margin-top:.2rem;">{{ _('JPEG/PNG, max 1200 px, auto-resized.') }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom:1rem;">
|
||||
<label style="display:block;font-size:.85rem;font-weight:600;color:#444;margin-bottom:.3rem;">Display name</label>
|
||||
<label style="display:block;font-size:.85rem;font-weight:600;color:#444;margin-bottom:.3rem;">{{ _('Display name') }}</label>
|
||||
<input type="text" name="display_name"
|
||||
value="{{ current_user.display_name or '' }}"
|
||||
required
|
||||
@@ -38,13 +38,13 @@
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom:1rem;">
|
||||
<label style="display:block;font-size:.85rem;font-weight:600;color:#444;margin-bottom:.3rem;">Bio</label>
|
||||
<textarea name="bio" rows="4" placeholder="Tell others a bit about yourself…"
|
||||
<label style="display:block;font-size:.85rem;font-weight:600;color:#444;margin-bottom:.3rem;">{{ _('Bio') }}</label>
|
||||
<textarea name="bio" rows="4" placeholder="{{ _('Tell others a bit about yourself…') }}"
|
||||
style="width:100%;padding:.55rem .75rem;border:1px solid #ccc;border-radius:4px;font-size:0.95rem;resize:vertical;">{{ current_user.bio or '' }}</textarea>
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom:1rem;">
|
||||
<label style="display:block;font-size:.85rem;font-weight:600;color:#444;margin-bottom:.3rem;">Email</label>
|
||||
<label style="display:block;font-size:.85rem;font-weight:600;color:#444;margin-bottom:.3rem;">{{ _('Email') }}</label>
|
||||
<input type="text" value="{{ current_user.email }}" disabled
|
||||
style="width:100%;padding:.55rem .75rem;border:1px solid #e0e0e0;border-radius:4px;font-size:0.95rem;background:#f5f5f5;color:#888;">
|
||||
<div style="font-size:0.78rem;color:#aaa;margin-top:.2rem;">
|
||||
@@ -57,49 +57,49 @@
|
||||
<input type="checkbox" name="show_equipment_public"
|
||||
{% if current_user.show_equipment_public %}checked{% endif %}
|
||||
style="width:1rem;height:1rem;">
|
||||
Show my equipment on my public profile
|
||||
{{ _('Show my equipment on my public profile') }}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div style="display:flex;align-items:center;gap:1.5rem;flex-wrap:wrap;">
|
||||
<button type="submit"
|
||||
style="background:#1a1a2e;color:#fff;border:none;border-radius:4px;padding:.6rem 1.5rem;font-size:.95rem;cursor:pointer;">
|
||||
Save changes
|
||||
{{ _('Save changes') }}
|
||||
</button>
|
||||
<a href="{{ url_for('public_profile', user_id=current_user.id) }}"
|
||||
style="font-size:0.9rem;color:#1f77b4;" target="_blank">
|
||||
View my public profile →
|
||||
{{ _('View my public profile →') }}
|
||||
</a>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{# ---- Change password (local accounts only) ---- #}
|
||||
{% if current_user.provider == 'local' %}
|
||||
<h2>Change password</h2>
|
||||
<h2>{{ _('Change password') }}</h2>
|
||||
<form method="post" action="{{ url_for('auth.profile') }}"
|
||||
style="max-width:480px;">
|
||||
<input type="hidden" name="action" value="change_password">
|
||||
|
||||
<div style="margin-bottom:1rem;">
|
||||
<label style="display:block;font-size:.85rem;font-weight:600;color:#444;margin-bottom:.3rem;">Current password</label>
|
||||
<label style="display:block;font-size:.85rem;font-weight:600;color:#444;margin-bottom:.3rem;">{{ _('Current password') }}</label>
|
||||
<input type="password" name="current_password" required
|
||||
style="width:100%;padding:.55rem .75rem;border:1px solid #ccc;border-radius:4px;font-size:.95rem;">
|
||||
</div>
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:1rem;margin-bottom:1rem;">
|
||||
<div>
|
||||
<label style="display:block;font-size:.85rem;font-weight:600;color:#444;margin-bottom:.3rem;">New password</label>
|
||||
<label style="display:block;font-size:.85rem;font-weight:600;color:#444;margin-bottom:.3rem;">{{ _('New password') }}</label>
|
||||
<input type="password" name="new_password" required minlength="8"
|
||||
style="width:100%;padding:.55rem .75rem;border:1px solid #ccc;border-radius:4px;font-size:.95rem;">
|
||||
</div>
|
||||
<div>
|
||||
<label style="display:block;font-size:.85rem;font-weight:600;color:#444;margin-bottom:.3rem;">Confirm</label>
|
||||
<label style="display:block;font-size:.85rem;font-weight:600;color:#444;margin-bottom:.3rem;">{{ _('Confirm') }}</label>
|
||||
<input type="password" name="confirm_password" required minlength="8"
|
||||
style="width:100%;padding:.55rem .75rem;border:1px solid #ccc;border-radius:4px;font-size:.95rem;">
|
||||
</div>
|
||||
</div>
|
||||
<button type="submit"
|
||||
style="background:#1a1a2e;color:#fff;border:none;border-radius:4px;padding:.6rem 1.5rem;font-size:.95rem;cursor:pointer;">
|
||||
Change password
|
||||
{{ _('Change password') }}
|
||||
</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
<div>
|
||||
<h1 style="margin:0 0 .25rem;">{{ profile_user.display_name or profile_user.email.split('@')[0] }}</h1>
|
||||
<div style="font-size:0.85rem;color:#888;">
|
||||
Member since {{ profile_user.created_at.strftime('%B %Y') }}
|
||||
{{ _('Member since %(date)s', date=profile_user.created_at.strftime('%B %Y')) }}
|
||||
</div>
|
||||
{% if profile_user.bio %}
|
||||
<p style="margin-top:.75rem;color:#444;white-space:pre-wrap;max-width:600px;">{{ profile_user.bio }}</p>
|
||||
@@ -24,12 +24,12 @@
|
||||
</div>
|
||||
|
||||
{# ---- Public Sessions ---- #}
|
||||
<h2>Sessions{% if public_sessions %} ({{ public_sessions|length }}){% endif %}</h2>
|
||||
<h2>{{ _('Sessions') }}{% if public_sessions %} ({{ public_sessions|length }}){% endif %}</h2>
|
||||
|
||||
{% if public_sessions %}
|
||||
<table style="margin-bottom:1.5rem;">
|
||||
<thead>
|
||||
<tr><th>Session</th><th>Location</th><th>Distance</th></tr>
|
||||
<tr><th>{{ _('Session') }}</th><th>{{ _('Location') }}</th><th>{{ _('Distance') }}</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for s in public_sessions %}
|
||||
@@ -46,16 +46,16 @@
|
||||
</tbody>
|
||||
</table>
|
||||
{% else %}
|
||||
<p style="color:#888;margin-bottom:1.5rem;">No public sessions yet.</p>
|
||||
<p style="color:#888;margin-bottom:1.5rem;">{{ _('No public sessions yet.') }}</p>
|
||||
{% endif %}
|
||||
|
||||
{# ---- Equipment (optional) ---- #}
|
||||
{% if equipment is not none %}
|
||||
<h2>Equipment</h2>
|
||||
<h2>{{ _('Equipment') }}</h2>
|
||||
{% if equipment %}
|
||||
<table style="margin-bottom:1.5rem;">
|
||||
<thead>
|
||||
<tr><th>Name</th><th>Category</th><th>Brand / Model</th><th>Caliber</th></tr>
|
||||
<tr><th>{{ _('Name') }}</th><th>{{ _('Category') }}</th><th>{{ _('Brand / Model') }}</th><th>{{ _('Caliber') }}</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for item in equipment %}
|
||||
@@ -73,7 +73,7 @@
|
||||
</tbody>
|
||||
</table>
|
||||
{% else %}
|
||||
<p style="color:#888;margin-bottom:1.5rem;">No equipment listed.</p>
|
||||
<p style="color:#888;margin-bottom:1.5rem;">{{ _('No equipment listed.') }}</p>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
|
||||
@@ -1,33 +1,33 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Create account — Ballistic Analyzer{% endblock %}
|
||||
{% block title %}{{ _('Create account') }} — Ballistic Analyzer{% endblock %}
|
||||
{% block content %}
|
||||
<h1>Create account</h1>
|
||||
<h1>{{ _('Create account') }}</h1>
|
||||
|
||||
<form method="post" action="{{ url_for('auth.register') }}" style="max-width:360px;">
|
||||
<div style="margin-bottom:1rem;">
|
||||
<label style="display:block;font-size:0.88rem;font-weight:600;color:#444;margin-bottom:0.3rem;">Email</label>
|
||||
<label style="display:block;font-size:0.88rem;font-weight:600;color:#444;margin-bottom:0.3rem;">{{ _('Email') }}</label>
|
||||
<input type="email" name="email" value="{{ prefill_email or '' }}" required autocomplete="email"
|
||||
style="width:100%;padding:0.55rem 0.75rem;border:1px solid #ccc;border-radius:4px;font-size:0.95rem;">
|
||||
</div>
|
||||
<div style="margin-bottom:1rem;">
|
||||
<label style="display:block;font-size:0.88rem;font-weight:600;color:#444;margin-bottom:0.3rem;">Password
|
||||
<span style="font-weight:400;color:#888;">(min. 8 characters)</span>
|
||||
<label style="display:block;font-size:0.88rem;font-weight:600;color:#444;margin-bottom:0.3rem;">{{ _('Password') }}
|
||||
<span style="font-weight:400;color:#888;">{{ _('(min. 8 characters)') }}</span>
|
||||
</label>
|
||||
<input type="password" name="password" required autocomplete="new-password" minlength="8"
|
||||
style="width:100%;padding:0.55rem 0.75rem;border:1px solid #ccc;border-radius:4px;font-size:0.95rem;">
|
||||
</div>
|
||||
<div style="margin-bottom:1.5rem;">
|
||||
<label style="display:block;font-size:0.88rem;font-weight:600;color:#444;margin-bottom:0.3rem;">Confirm password</label>
|
||||
<label style="display:block;font-size:0.88rem;font-weight:600;color:#444;margin-bottom:0.3rem;">{{ _('Confirm password') }}</label>
|
||||
<input type="password" name="confirm_password" required autocomplete="new-password"
|
||||
style="width:100%;padding:0.55rem 0.75rem;border:1px solid #ccc;border-radius:4px;font-size:0.95rem;">
|
||||
</div>
|
||||
<button type="submit"
|
||||
style="width:100%;background:#1a1a2e;color:#fff;border:none;border-radius:4px;padding:0.65rem;font-size:0.95rem;cursor:pointer;">
|
||||
Create account
|
||||
{{ _('Create account') }}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<p style="font-size:0.9rem;color:#555;margin-top:1.25rem;">
|
||||
Already have an account? <a href="{{ url_for('auth.login') }}">Sign in</a>
|
||||
{{ _('Already have an account?') }} <a href="{{ url_for('auth.login') }}">{{ _('Sign in') }}</a>
|
||||
</p>
|
||||
{% endblock %}
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
padding: 0 1.5rem;
|
||||
height: 52px;
|
||||
gap: 1rem;
|
||||
position: relative;
|
||||
}
|
||||
.nav-brand {
|
||||
font-weight: 700;
|
||||
@@ -57,6 +58,57 @@
|
||||
gap: 0.75rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
/* ── Hamburger (hidden on desktop) ── */
|
||||
.nav-hamburger {
|
||||
display: none;
|
||||
background: none;
|
||||
border: none;
|
||||
color: #fff;
|
||||
font-size: 1.5rem;
|
||||
line-height: 1;
|
||||
cursor: pointer;
|
||||
padding: 0.25rem 0.4rem;
|
||||
}
|
||||
/* ── Mobile menu panel ── */
|
||||
.nav-mobile-menu {
|
||||
display: none;
|
||||
position: absolute;
|
||||
top: 52px;
|
||||
left: 0; right: 0;
|
||||
background: #1a1a2e;
|
||||
border-top: 1px solid rgba(255,255,255,.12);
|
||||
padding: 0.75rem 1.5rem 1rem;
|
||||
z-index: 200;
|
||||
flex-direction: column;
|
||||
gap: 0;
|
||||
}
|
||||
.nav-mobile-menu a,
|
||||
.nav-mobile-menu button {
|
||||
display: block;
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
color: #c8cfe0;
|
||||
text-decoration: none;
|
||||
font-size: 0.95rem;
|
||||
padding: 0.6rem 0;
|
||||
border-bottom: 1px solid rgba(255,255,255,.07);
|
||||
background: none;
|
||||
border-left: none;
|
||||
border-right: none;
|
||||
border-top: none;
|
||||
cursor: pointer;
|
||||
font-family: inherit;
|
||||
}
|
||||
.nav-mobile-menu a:last-child,
|
||||
.nav-mobile-menu button:last-child { border-bottom: none; }
|
||||
.nav-mobile-menu a:hover,
|
||||
.nav-mobile-menu button:hover { color: #fff; }
|
||||
.nav.open .nav-mobile-menu { display: flex; }
|
||||
@media (max-width: 640px) {
|
||||
.nav-links { display: none; }
|
||||
.nav-right { display: none; }
|
||||
.nav-hamburger { display: block; }
|
||||
}
|
||||
/* ── User dropdown ── */
|
||||
.nav-dropdown { position: relative; }
|
||||
.nav-user-btn {
|
||||
@@ -189,19 +241,30 @@
|
||||
<body>
|
||||
|
||||
{% block body %}
|
||||
<nav class="nav">
|
||||
<nav class="nav" id="mainNav">
|
||||
<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>
|
||||
<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">
|
||||
{# Language switcher #}
|
||||
<div class="nav-dropdown" id="langDropdown">
|
||||
<button class="nav-user-btn" onclick="toggleLangDropdown(event)" style="padding:.2rem .55rem;gap:.35rem;font-size:1.1rem;">
|
||||
{% if current_lang == 'fr' %}🇫🇷{% elif current_lang == 'de' %}🇩🇪{% else %}🇬🇧{% endif %}<span class="caret">▼</span>
|
||||
</button>
|
||||
<div class="nav-dd-menu" style="min-width:130px;">
|
||||
<a href="{{ url_for('set_lang', lang='en') }}">🇬🇧 English</a>
|
||||
<a href="{{ url_for('set_lang', lang='fr') }}">🇫🇷 Français</a>
|
||||
<a href="{{ url_for('set_lang', lang='de') }}">🇩🇪 Deutsch</a>
|
||||
</div>
|
||||
</div>
|
||||
{% if current_user.is_authenticated %}
|
||||
<div class="nav-dropdown" id="userDropdown">
|
||||
<button class="nav-user-btn" onclick="toggleDropdown(event)">
|
||||
@@ -215,21 +278,46 @@
|
||||
<span class="caret">▼</span>
|
||||
</button>
|
||||
<div class="nav-dd-menu">
|
||||
<a href="{{ url_for('auth.profile') }}">👤 Profile</a>
|
||||
<a href="{{ url_for('auth.profile') }}">👤 {{ _('Profile') }}</a>
|
||||
<hr>
|
||||
<form method="post" action="{{ url_for('auth.logout') }}">
|
||||
<button type="submit">→ Logout</button>
|
||||
<button type="submit">→ {{ _('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.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
|
||||
{{ _('Join free') }}
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{# Hamburger — only visible on mobile #}
|
||||
<button class="nav-hamburger" onclick="toggleMobileNav(event)" aria-label="Menu">☰</button>
|
||||
|
||||
{# Mobile menu panel #}
|
||||
<div class="nav-mobile-menu">
|
||||
{% 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>
|
||||
<a href="{{ url_for('auth.profile') }}">{{ _('Profile') }}</a>
|
||||
<form method="post" action="{{ url_for('auth.logout') }}" style="padding:0;border:none;">
|
||||
<button type="submit">{{ _('Logout') }}</button>
|
||||
</form>
|
||||
{% else %}
|
||||
<a href="{{ url_for('auth.login') }}">{{ _('Login') }}</a>
|
||||
<a href="{{ url_for('auth.register') }}">{{ _('Join free') }}</a>
|
||||
{% endif %}
|
||||
<div style="padding:.5rem 0;border-top:1px solid rgba(255,255,255,.1);margin-top:.25rem;display:flex;gap:1rem;">
|
||||
<a href="{{ url_for('set_lang', lang='en') }}" style="font-size:1.1rem;text-decoration:none;color:#fff;">🇬🇧</a>
|
||||
<a href="{{ url_for('set_lang', lang='fr') }}" style="font-size:1.1rem;text-decoration:none;color:#fff;">🇫🇷</a>
|
||||
<a href="{{ url_for('set_lang', lang='de') }}" style="font-size:1.1rem;text-decoration:none;color:#fff;">🇩🇪</a>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<script>
|
||||
@@ -237,9 +325,21 @@
|
||||
e.stopPropagation();
|
||||
document.getElementById('userDropdown').classList.toggle('open');
|
||||
}
|
||||
function toggleLangDropdown(e) {
|
||||
e.stopPropagation();
|
||||
document.getElementById('langDropdown').classList.toggle('open');
|
||||
}
|
||||
function toggleMobileNav(e) {
|
||||
e.stopPropagation();
|
||||
document.getElementById('mainNav').classList.toggle('open');
|
||||
}
|
||||
document.addEventListener('click', function() {
|
||||
var d = document.getElementById('userDropdown');
|
||||
if (d) d.classList.remove('open');
|
||||
var l = document.getElementById('langDropdown');
|
||||
if (l) l.classList.remove('open');
|
||||
var n = document.getElementById('mainNav');
|
||||
if (n) n.classList.remove('open');
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
@@ -1,37 +1,37 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Dashboard — The Shooter's Network{% endblock %}
|
||||
{% block title %}{{ _('Dashboard') }} — The Shooter's Network{% endblock %}
|
||||
{% block content %}
|
||||
<h1>Dashboard</h1>
|
||||
<h1>{{ _('Dashboard') }}</h1>
|
||||
<p style="color:#555;margin-bottom:2rem;">
|
||||
Welcome back, <strong>{{ current_user.display_name or current_user.email }}</strong>.
|
||||
{{ _('Welcome back, %(name)s.', name=(current_user.display_name or current_user.email)) }}
|
||||
</p>
|
||||
|
||||
<div style="display:flex;gap:1.5rem;margin-bottom:2.5rem;flex-wrap:wrap;">
|
||||
<a href="{{ url_for('sessions.new') }}"
|
||||
style="background:#1a1a2e;color:#fff;padding:0.5rem 1.2rem;border-radius:4px;font-size:0.92rem;text-decoration:none;">
|
||||
+ New session
|
||||
{{ _('+ New session') }}
|
||||
</a>
|
||||
<a href="{{ url_for('equipment.new') }}"
|
||||
style="background:#f0f4ff;color:#1a1a2e;padding:0.5rem 1.2rem;border-radius:4px;font-size:0.92rem;text-decoration:none;border:1px solid #c0d0f0;">
|
||||
+ Add equipment
|
||||
{{ _('+ Add equipment') }}
|
||||
</a>
|
||||
<a href="{{ url_for('analyze') }}"
|
||||
style="background:#f0f4ff;color:#1a1a2e;padding:0.5rem 1.2rem;border-radius:4px;font-size:0.92rem;text-decoration:none;border:1px solid #c0d0f0;">
|
||||
New analysis
|
||||
{{ _('New analysis') }}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<h2>Recent Analyses</h2>
|
||||
<h2>{{ _('Recent Analyses') }}</h2>
|
||||
|
||||
{% if analyses %}
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Title</th>
|
||||
<th>Date</th>
|
||||
<th>Shots</th>
|
||||
<th>Groups</th>
|
||||
<th>Visibility</th>
|
||||
<th>{{ _('Title') }}</th>
|
||||
<th>{{ _('Date') }}</th>
|
||||
<th>{{ _('Shots') }}</th>
|
||||
<th>{{ _('Groups') }}</th>
|
||||
<th>{{ _('Visibility') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -42,7 +42,7 @@
|
||||
<td>{{ a.shot_count }}</td>
|
||||
<td>{{ a.group_count }}</td>
|
||||
<td style="color:{% if a.is_public %}#27ae60{% else %}#888{% endif %};font-size:0.88rem;">
|
||||
{{ 'Public' if a.is_public else 'Private' }}
|
||||
{{ _('Public') if a.is_public else _('Private') }}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
@@ -50,18 +50,18 @@
|
||||
</table>
|
||||
{% else %}
|
||||
<p style="color:#888;margin-top:1rem;">
|
||||
No analyses yet. <a href="{{ url_for('analyze') }}">Upload a CSV file</a> to get started — it will be saved here automatically.
|
||||
{{ _('No analyses yet.') }} <a href="{{ url_for('analyze') }}">{{ _('Upload a CSV file') }}</a> {{ _('to get started — it will be saved here automatically.') }}
|
||||
</p>
|
||||
{% endif %}
|
||||
|
||||
<div style="display:flex;gap:2rem;margin-top:2.5rem;flex-wrap:wrap;">
|
||||
<div style="flex:1;min-width:180px;padding:1.25rem;border:1px solid #e0e0e0;border-radius:6px;">
|
||||
<div style="font-size:0.8rem;color:#888;text-transform:uppercase;letter-spacing:.05em;margin-bottom:.4rem;">Equipment</div>
|
||||
<a href="{{ url_for('equipment.index') }}">Manage your rifles, scopes & gear →</a>
|
||||
<div style="font-size:0.8rem;color:#888;text-transform:uppercase;letter-spacing:.05em;margin-bottom:.4rem;">{{ _('Equipment') }}</div>
|
||||
<a href="{{ url_for('equipment.index') }}">{{ _('Manage your rifles, scopes & gear →') }}</a>
|
||||
</div>
|
||||
<div style="flex:1;min-width:180px;padding:1.25rem;border:1px solid #e0e0e0;border-radius:6px;">
|
||||
<div style="font-size:0.8rem;color:#888;text-transform:uppercase;letter-spacing:.05em;margin-bottom:.4rem;">Sessions</div>
|
||||
<a href="{{ url_for('sessions.index') }}">View your shooting sessions →</a>
|
||||
<div style="font-size:0.8rem;color:#888;text-transform:uppercase;letter-spacing:.05em;margin-bottom:.4rem;">{{ _('Sessions') }}</div>
|
||||
<a href="{{ url_for('sessions.index') }}">{{ _('View your shooting sessions →') }}</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
{% extends "base.html" %}
|
||||
{% set editing = item is not none %}
|
||||
{% block title %}{{ 'Edit' if editing else 'Add' }} Equipment — The Shooter's Network{% endblock %}
|
||||
{% block title %}{{ _('Edit') if editing else _('Add equipment') }} — The Shooter's Network{% endblock %}
|
||||
{% block content %}
|
||||
<h1>{{ 'Edit' if editing else 'Add equipment' }}</h1>
|
||||
<h1>{{ _('Edit') if editing else _('Add equipment') }}</h1>
|
||||
|
||||
{% set f = prefill or item %}
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
style="max-width:520px;">
|
||||
|
||||
<div style="margin-bottom:1rem;">
|
||||
<label class="field-label">Category *</label>
|
||||
<label class="field-label">{{ _('Category *') }}</label>
|
||||
<select name="category" required style="width:100%;padding:0.55rem 0.75rem;border:1px solid #ccc;border-radius:4px;font-size:0.95rem;background:#fff;">
|
||||
{% for key, label in categories %}
|
||||
<option value="{{ key }}" {% if (f and f.category == key) or (not f and key == 'rifle') %}selected{% endif %}>{{ label }}</option>
|
||||
@@ -21,7 +21,7 @@
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom:1rem;">
|
||||
<label class="field-label">Name *</label>
|
||||
<label class="field-label">{{ _('Name *') }}</label>
|
||||
<input type="text" name="name" value="{{ f.name if f else '' }}" required
|
||||
placeholder="e.g. Tikka T3x, Glock 17"
|
||||
style="width:100%;padding:0.55rem 0.75rem;border:1px solid #ccc;border-radius:4px;font-size:0.95rem;">
|
||||
@@ -29,13 +29,13 @@
|
||||
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:1rem;margin-bottom:1rem;">
|
||||
<div>
|
||||
<label class="field-label">Brand</label>
|
||||
<label class="field-label">{{ _('Brand') }}</label>
|
||||
<input type="text" name="brand" value="{{ f.brand if f else '' }}"
|
||||
placeholder="e.g. Tikka, Leupold"
|
||||
style="width:100%;padding:0.55rem 0.75rem;border:1px solid #ccc;border-radius:4px;font-size:0.95rem;">
|
||||
</div>
|
||||
<div>
|
||||
<label class="field-label">Model</label>
|
||||
<label class="field-label">{{ _('Model') }}</label>
|
||||
<input type="text" name="model" value="{{ f.model if f else '' }}"
|
||||
placeholder="e.g. T3x, VX-3HD"
|
||||
style="width:100%;padding:0.55rem 0.75rem;border:1px solid #ccc;border-radius:4px;font-size:0.95rem;">
|
||||
@@ -45,7 +45,7 @@
|
||||
<div id="rifle-fields" style="margin-bottom:1rem;">
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:1rem;">
|
||||
<div>
|
||||
<label class="field-label">Caliber</label>
|
||||
<label class="field-label">{{ _('Caliber') }}</label>
|
||||
<input type="text" name="caliber" value="{{ f.caliber if f else '' }}"
|
||||
placeholder="e.g. .308 Win, 6.5 CM"
|
||||
style="width:100%;padding:0.55rem 0.75rem;border:1px solid #ccc;border-radius:4px;font-size:0.95rem;">
|
||||
@@ -56,13 +56,13 @@
|
||||
<div id="scope-fields" style="display:none;margin-bottom:1rem;">
|
||||
<div style="display:grid;grid-template-columns:repeat(3,1fr);gap:1rem;">
|
||||
<div>
|
||||
<label class="field-label">Magnification</label>
|
||||
<label class="field-label">{{ _('Magnification') }}</label>
|
||||
<input type="text" name="magnification" value="{{ f.magnification if f else '' }}"
|
||||
placeholder="e.g. 3-15x50"
|
||||
style="width:100%;padding:0.55rem 0.75rem;border:1px solid #ccc;border-radius:4px;font-size:0.95rem;">
|
||||
</div>
|
||||
<div>
|
||||
<label class="field-label">Reticle</label>
|
||||
<label class="field-label">{{ _('Reticle') }}</label>
|
||||
<select name="reticle" style="width:100%;padding:0.55rem 0.75rem;border:1px solid #ccc;border-radius:4px;font-size:0.95rem;background:#fff;">
|
||||
<option value="">—</option>
|
||||
<option value="FFP" {% if f and f.reticle == 'FFP' %}selected{% endif %}>FFP (First Focal Plane)</option>
|
||||
@@ -70,7 +70,7 @@
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="field-label">Unit</label>
|
||||
<label class="field-label">{{ _('Unit') }}</label>
|
||||
<select name="unit" style="width:100%;padding:0.55rem 0.75rem;border:1px solid #ccc;border-radius:4px;font-size:0.95rem;background:#fff;">
|
||||
<option value="">—</option>
|
||||
<option value="MOA" {% if f and f.unit == 'MOA' %}selected{% endif %}>MOA</option>
|
||||
@@ -81,24 +81,24 @@
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom:1rem;">
|
||||
<label class="field-label">Serial number</label>
|
||||
<label class="field-label">{{ _('Serial number') }}</label>
|
||||
<input type="text" name="serial_number" value="{{ f.serial_number if f else '' }}"
|
||||
style="width:100%;padding:0.55rem 0.75rem;border:1px solid #ccc;border-radius:4px;font-size:0.95rem;">
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom:1rem;">
|
||||
<label class="field-label">Notes</label>
|
||||
<label class="field-label">{{ _('Notes') }}</label>
|
||||
<textarea name="notes" rows="3"
|
||||
style="width:100%;padding:0.55rem 0.75rem;border:1px solid #ccc;border-radius:4px;font-size:0.95rem;resize:vertical;">{{ f.notes if f else '' }}</textarea>
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom:1.5rem;">
|
||||
<label class="field-label">Photo</label>
|
||||
<label class="field-label">{{ _('Photo') }}</label>
|
||||
{% if editing and item.photo_url %}
|
||||
<div style="margin-bottom:0.5rem;">
|
||||
<img src="{{ item.photo_url }}" alt="Current photo"
|
||||
style="height:80px;border-radius:4px;object-fit:cover;">
|
||||
<span style="font-size:0.82rem;color:#888;margin-left:0.5rem;">Upload a new one to replace it.</span>
|
||||
<span style="font-size:0.82rem;color:#888;margin-left:0.5rem;">{{ _('Upload a new one to replace it.') }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
<input type="file" name="photo" accept="image/*"
|
||||
@@ -108,10 +108,10 @@
|
||||
<div style="display:flex;gap:1rem;align-items:center;">
|
||||
<button type="submit"
|
||||
style="background:#1a1a2e;color:#fff;border:none;border-radius:4px;padding:0.6rem 1.5rem;font-size:0.95rem;cursor:pointer;">
|
||||
{{ 'Save changes' if editing else 'Add equipment' }}
|
||||
{{ _('Save changes') if editing else _('Add equipment') }}
|
||||
</button>
|
||||
<a href="{{ url_for('equipment.detail', item_id=item.id) if editing else url_for('equipment.index') }}"
|
||||
style="font-size:0.9rem;color:#666;">Cancel</a>
|
||||
style="font-size:0.9rem;color:#666;">{{ _('Cancel') }}</a>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Equipment — The Shooter's Network{% endblock %}
|
||||
{% block title %}{{ _('Equipment') }} — The Shooter's Network{% endblock %}
|
||||
{% block content %}
|
||||
<div style="display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:1rem;margin-bottom:1.5rem;">
|
||||
<h1 style="margin:0;">My Equipment</h1>
|
||||
<h1 style="margin:0;">{{ _('My Equipment') }}</h1>
|
||||
<a href="{{ url_for('equipment.new') }}"
|
||||
style="background:#1a1a2e;color:#fff;padding:0.5rem 1.2rem;border-radius:4px;font-size:0.92rem;text-decoration:none;">
|
||||
+ Add item
|
||||
{{ _('+ Add item') }}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@@ -39,11 +39,11 @@
|
||||
<div style="font-size:0.82rem;color:#888;margin-bottom:0.6rem;">{{ item.caliber }}</div>
|
||||
{% endif %}
|
||||
<div style="display:flex;gap:0.75rem;margin-top:0.5rem;">
|
||||
<a href="{{ url_for('equipment.detail', item_id=item.id) }}" style="font-size:0.85rem;">View</a>
|
||||
<a href="{{ url_for('equipment.edit', item_id=item.id) }}" style="font-size:0.85rem;">Edit</a>
|
||||
<a href="{{ url_for('equipment.detail', item_id=item.id) }}" style="font-size:0.85rem;">{{ _('View') }}</a>
|
||||
<a href="{{ url_for('equipment.edit', item_id=item.id) }}" style="font-size:0.85rem;">{{ _('Edit') }}</a>
|
||||
<form method="post" action="{{ url_for('equipment.delete', item_id=item.id) }}" style="display:inline;"
|
||||
onsubmit="return confirm('Delete {{ item.name }}?');">
|
||||
<button type="submit" class="btn-link" style="font-size:0.85rem;color:#e74c3c;">Delete</button>
|
||||
onsubmit="return confirm('{{ _('Delete %(name)s?', name=item.name) | e }}');">
|
||||
<button type="submit" class="btn-link" style="font-size:0.85rem;color:#e74c3c;">{{ _('Delete') }}</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
@@ -55,8 +55,8 @@
|
||||
{% else %}
|
||||
<div style="text-align:center;padding:3rem 0;color:#888;">
|
||||
<div style="font-size:3rem;margin-bottom:1rem;">🔫</div>
|
||||
<p style="margin-bottom:1rem;">No equipment yet.</p>
|
||||
<a href="{{ url_for('equipment.new') }}">Add your first item</a>
|
||||
<p style="margin-bottom:1rem;">{{ _('No equipment yet.') }}</p>
|
||||
<a href="{{ url_for('equipment.new') }}">{{ _('Add your first item') }}</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
@@ -6,13 +6,24 @@
|
||||
<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>
|
||||
<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">
|
||||
{# Language switcher #}
|
||||
<div class="nav-dropdown" id="langDropdown">
|
||||
<button class="nav-user-btn" onclick="toggleLangDropdown(event)" style="padding:.2rem .55rem;gap:.35rem;font-size:1.1rem;">
|
||||
{% if current_lang == 'fr' %}🇫🇷{% elif current_lang == 'de' %}🇩🇪{% else %}🇬🇧{% endif %}<span class="caret">▼</span>
|
||||
</button>
|
||||
<div class="nav-dd-menu" style="min-width:130px;">
|
||||
<a href="{{ url_for('set_lang', lang='en') }}">🇬🇧 English</a>
|
||||
<a href="{{ url_for('set_lang', lang='fr') }}">🇫🇷 Français</a>
|
||||
<a href="{{ url_for('set_lang', lang='de') }}">🇩🇪 Deutsch</a>
|
||||
</div>
|
||||
</div>
|
||||
{% if current_user.is_authenticated %}
|
||||
<div class="nav-dropdown" id="userDropdown">
|
||||
<button class="nav-user-btn" onclick="toggleDropdown(event)">
|
||||
@@ -23,18 +34,18 @@
|
||||
<span class="caret">▼</span>
|
||||
</button>
|
||||
<div class="nav-dd-menu">
|
||||
<a href="{{ url_for('auth.profile') }}">👤 Profile</a>
|
||||
<a href="{{ url_for('auth.profile') }}">👤 {{ _('Profile') }}</a>
|
||||
<hr>
|
||||
<form method="post" action="{{ url_for('auth.logout') }}">
|
||||
<button type="submit">→ Logout</button>
|
||||
<button type="submit">→ {{ _('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.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
|
||||
{{ _('Join free') }}
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
@@ -44,9 +55,15 @@ function toggleDropdown(e) {
|
||||
e.stopPropagation();
|
||||
document.getElementById('userDropdown').classList.toggle('open');
|
||||
}
|
||||
function toggleLangDropdown(e) {
|
||||
e.stopPropagation();
|
||||
document.getElementById('langDropdown').classList.toggle('open');
|
||||
}
|
||||
document.addEventListener('click', function() {
|
||||
var d = document.getElementById('userDropdown');
|
||||
if (d) d.classList.remove('open');
|
||||
var l = document.getElementById('langDropdown');
|
||||
if (l) l.classList.remove('open');
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -63,20 +80,20 @@ document.addEventListener('click', function() {
|
||||
{% 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
|
||||
{{ _('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
|
||||
{{ _('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
|
||||
{{ _('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
|
||||
{{ _('Try without account') }}
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
@@ -87,18 +104,18 @@ document.addEventListener('click', function() {
|
||||
<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>
|
||||
<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>
|
||||
<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>
|
||||
<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>
|
||||
@@ -118,7 +135,7 @@ document.addEventListener('click', function() {
|
||||
<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
|
||||
{{ _('Latest sessions') }}
|
||||
</h2>
|
||||
|
||||
{% if public_sessions %}
|
||||
@@ -149,7 +166,7 @@ document.addEventListener('click', function() {
|
||||
</div>
|
||||
{% else %}
|
||||
<p style="color:#aaa;text-align:center;padding:3rem 0;">
|
||||
No public sessions yet. Be the first to share one!
|
||||
{{ _('No public sessions yet. Be the first to share one!') }}
|
||||
</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
@@ -1,36 +1,36 @@
|
||||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
<div style="display:flex;align-items:baseline;justify-content:space-between;flex-wrap:wrap;gap:1rem;margin-bottom:1.5rem;">
|
||||
<h1 style="margin:0;">Analysis Results</h1>
|
||||
<h1 style="margin:0;">{{ _('Analysis Results') }}</h1>
|
||||
<div style="display:flex;gap:0.75rem;align-items:center;flex-wrap:wrap;">
|
||||
<a href="/">← Upload another file</a>
|
||||
<a href="/">{{ _('← Upload another file') }}</a>
|
||||
{% if saved_analysis_id %}
|
||||
<a href="{{ url_for('analyses.detail', analysis_id=saved_analysis_id) }}"
|
||||
style="font-size:0.9rem;color:#1f77b4;">View saved report →</a>
|
||||
style="font-size:0.9rem;color:#1f77b4;">{{ _('View saved report →') }}</a>
|
||||
{% endif %}
|
||||
<a href="data:application/pdf;base64,{{ pdf_b64 }}"
|
||||
download="ballistic_report.pdf"
|
||||
style="background:#1f77b4;color:#fff;border-radius:4px;padding:0.5rem 1.1rem;font-size:0.9rem;text-decoration:none;">
|
||||
⇓ Download PDF report
|
||||
{{ _('⬙ Download PDF report') }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2>Overall Statistics</h2>
|
||||
<h2>{{ _('Overall Statistics') }}</h2>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Metric</th>
|
||||
<th>Value</th>
|
||||
<th>{{ _('Metric') }}</th>
|
||||
<th>{{ _('Value') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr><td>Total shots</td><td>{{ overall.count }}</td></tr>
|
||||
<tr><td>Min speed</td><td>{{ "%.4f"|format(overall.min_speed) }}</td></tr>
|
||||
<tr><td>Max speed</td><td>{{ "%.4f"|format(overall.max_speed) }}</td></tr>
|
||||
<tr><td>Mean speed</td><td>{{ "%.4f"|format(overall.mean_speed) }}</td></tr>
|
||||
<tr><td>{{ _('Total shots') }}</td><td>{{ overall.count }}</td></tr>
|
||||
<tr><td>{{ _('Min speed') }}</td><td>{{ "%.4f"|format(overall.min_speed) }}</td></tr>
|
||||
<tr><td>{{ _('Max speed') }}</td><td>{{ "%.4f"|format(overall.max_speed) }}</td></tr>
|
||||
<tr><td>{{ _('Mean speed') }}</td><td>{{ "%.4f"|format(overall.mean_speed) }}</td></tr>
|
||||
<tr>
|
||||
<td>Std dev (speed)</td>
|
||||
<td>{{ _('Std dev (speed)') }}</td>
|
||||
<td>
|
||||
{% if overall.std_speed is not none %}
|
||||
{{ "%.4f"|format(overall.std_speed) }}
|
||||
@@ -45,27 +45,27 @@
|
||||
<img class="chart-img" src="data:image/png;base64,{{ overview_chart }}"
|
||||
alt="Avg speed and std dev per group" style="max-width:600px;margin:1rem 0 1.5rem;">
|
||||
|
||||
<h2>Groups — {{ groups_display|length }} group(s) detected</h2>
|
||||
<h2>{{ _('Groups') }} — {{ groups_display|length }} {{ _('group(s) detected') }}</h2>
|
||||
|
||||
{% for stat, chart_b64 in groups_display %}
|
||||
<div class="group-section">
|
||||
<h3>Group {{ stat.group_index }}</h3>
|
||||
<h3>{{ _('Group %(n)s', n=stat.group_index) }}</h3>
|
||||
<div class="group-meta">
|
||||
{{ stat.time_start }} – {{ stat.time_end }} | {{ stat.count }} shot(s)
|
||||
</div>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Metric</th>
|
||||
<th>Value</th>
|
||||
<th>{{ _('Metric') }}</th>
|
||||
<th>{{ _('Value') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr><td>Min speed</td><td>{{ "%.4f"|format(stat.min_speed) }}</td></tr>
|
||||
<tr><td>Max speed</td><td>{{ "%.4f"|format(stat.max_speed) }}</td></tr>
|
||||
<tr><td>Mean speed</td><td>{{ "%.4f"|format(stat.mean_speed) }}</td></tr>
|
||||
<tr><td>{{ _('Min speed') }}</td><td>{{ "%.4f"|format(stat.min_speed) }}</td></tr>
|
||||
<tr><td>{{ _('Max speed') }}</td><td>{{ "%.4f"|format(stat.max_speed) }}</td></tr>
|
||||
<tr><td>{{ _('Mean speed') }}</td><td>{{ "%.4f"|format(stat.mean_speed) }}</td></tr>
|
||||
<tr>
|
||||
<td>Std dev (speed)</td>
|
||||
<td>{{ _('Std dev (speed)') }}</td>
|
||||
<td>
|
||||
{% if stat.std_speed is not none %}
|
||||
{{ "%.4f"|format(stat.std_speed) }}
|
||||
|
||||
@@ -8,7 +8,14 @@
|
||||
<a href="{{ url_for('sessions.detail', session_id=session.id) }}">{{ session.label }}</a> ›
|
||||
Annotate
|
||||
</div>
|
||||
<h1 style="margin:0;">{{ photo.caption or 'Photo annotation' }}</h1>
|
||||
<div style="display:flex;align-items:center;justify-content:space-between;gap:1rem;">
|
||||
<h1 style="margin:0;">{{ photo.caption or 'Photo annotation' }}</h1>
|
||||
<a href="{{ url_for('sessions.detail', session_id=session.id) }}"
|
||||
style="background:#f0f4ff;color:#1a1a2e;border:1px solid #c8d4f0;border-radius:4px;
|
||||
padding:.45rem 1rem;font-size:0.88rem;text-decoration:none;white-space:nowrap;">
|
||||
✕ Close
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="display:flex;gap:1.5rem;align-items:flex-start;">
|
||||
@@ -49,8 +56,8 @@
|
||||
|
||||
<hr style="border:none;border-top:1px solid #e8e8e8;margin-bottom:1rem;">
|
||||
|
||||
{# Shooting distance (always visible) #}
|
||||
<div style="margin-bottom:1rem;">
|
||||
{# Shooting distance (always visible, pre-filled from session) #}
|
||||
<div style="margin-bottom:.75rem;">
|
||||
<label style="display:block;font-size:.82rem;font-weight:600;color:#444;margin-bottom:.3rem;">Shooting distance</label>
|
||||
<div style="display:flex;gap:.4rem;">
|
||||
<input type="number" id="shoot-dist" min="1" step="1" placeholder="100"
|
||||
@@ -63,6 +70,14 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# Clean barrel checkbox #}
|
||||
<div style="margin-bottom:1rem;">
|
||||
<label style="display:flex;align-items:center;gap:.5rem;cursor:pointer;font-size:0.88rem;color:#444;">
|
||||
<input type="checkbox" id="clean-barrel" style="width:1rem;height:1rem;">
|
||||
Clean barrel (first shot)
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<hr style="border:none;border-top:1px solid #e8e8e8;margin-bottom:1rem;">
|
||||
|
||||
{# Step 0: Reference line #}
|
||||
@@ -117,7 +132,7 @@
|
||||
<div id="panel-3" class="step-panel" style="display:none;">
|
||||
<div id="results-box" style="margin-bottom:1rem;"></div>
|
||||
<div style="display:flex;gap:.5rem;flex-wrap:wrap;">
|
||||
<button class="btn-primary" id="btn-save" onclick="saveAnnotations()">Save</button>
|
||||
<button class="btn-primary" id="btn-save" onclick="saveAnnotations()">Save & close</button>
|
||||
<button class="btn-ghost" onclick="goStep(2)">← Edit</button>
|
||||
</div>
|
||||
<div id="save-status" style="font-size:0.82rem;margin-top:.5rem;"></div>
|
||||
@@ -145,9 +160,11 @@
|
||||
</style>
|
||||
|
||||
<script>
|
||||
const PHOTO_URL = {{ photo.photo_url | tojson }};
|
||||
const SAVE_URL = {{ url_for('sessions.annotate_photo', session_id=session.id, photo_id=photo.id) | tojson }};
|
||||
const EXISTING = {{ (photo.annotations or {}) | tojson }};
|
||||
const PHOTO_URL = {{ photo.photo_url | tojson }};
|
||||
const SAVE_URL = {{ url_for('sessions.annotate_photo', session_id=session.id, photo_id=photo.id) | tojson }};
|
||||
const SESSION_URL = {{ url_for('sessions.detail', session_id=session.id) | tojson }};
|
||||
const SESSION_DIST_M = {{ (session.distance_m or 'null') }};
|
||||
const EXISTING = {{ (photo.annotations or {}) | tojson }};
|
||||
|
||||
// ── State ──────────────────────────────────────────────────────────────────
|
||||
let step = 0;
|
||||
@@ -408,6 +425,8 @@ function renderResults() {
|
||||
<span class="stat-val">${stats.shot_count}</span></div>
|
||||
<div class="stat-row"><span class="stat-label">@ distance</span>
|
||||
<span class="stat-val">${stats.shooting_distance_m.toFixed(0)} m</span></div>
|
||||
<div class="stat-row"><span class="stat-label">Clean barrel</span>
|
||||
<span class="stat-val">${document.getElementById('clean-barrel').checked ? 'Yes' : 'No'}</span></div>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -554,6 +573,7 @@ async function saveAnnotations() {
|
||||
poa: toFrac(poa),
|
||||
pois: pois.map(toFrac),
|
||||
shooting_distance_m: stats.shooting_distance_m,
|
||||
clean_barrel: document.getElementById('clean-barrel').checked,
|
||||
stats: stats,
|
||||
};
|
||||
|
||||
@@ -564,8 +584,7 @@ async function saveAnnotations() {
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
if (resp.ok) {
|
||||
status.style.color = '#27ae60';
|
||||
status.textContent = 'Saved!';
|
||||
window.location.href = SESSION_URL;
|
||||
} else {
|
||||
throw new Error('Server error');
|
||||
}
|
||||
@@ -578,6 +597,12 @@ async function saveAnnotations() {
|
||||
|
||||
// ── Load existing annotations ──────────────────────────────────────────────
|
||||
function loadExisting() {
|
||||
// Always pre-fill shooting distance from session if available
|
||||
if (SESSION_DIST_M) {
|
||||
document.getElementById('shoot-dist').value = SESSION_DIST_M;
|
||||
document.getElementById('shoot-unit').value = 'm';
|
||||
}
|
||||
|
||||
if (!EXISTING || !EXISTING.ref) return;
|
||||
const W = img.naturalWidth, H = img.naturalHeight;
|
||||
function fromFrac(f) { return { x: f.x * W, y: f.y * H }; }
|
||||
@@ -597,6 +622,9 @@ function loadExisting() {
|
||||
document.getElementById('shoot-dist').value = EXISTING.shooting_distance_m.toFixed(0);
|
||||
document.getElementById('shoot-unit').value = 'm';
|
||||
}
|
||||
if (EXISTING.clean_barrel) {
|
||||
document.getElementById('clean-barrel').checked = true;
|
||||
}
|
||||
if (EXISTING.stats) {
|
||||
stats = EXISTING.stats;
|
||||
renderResults();
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
{% if is_owner %}<a href="{{ url_for('sessions.index') }}">Sessions</a> › {% endif %}
|
||||
{{ session.session_date.strftime('%d %b %Y') }}
|
||||
{% if session.is_public %}
|
||||
<span style="background:#e8f5e9;color:#27ae60;font-size:0.75rem;padding:.1rem .45rem;border-radius:3px;margin-left:.4rem;">Public</span>
|
||||
<span style="background:#e8f5e9;color:#27ae60;font-size:0.75rem;padding:.1rem .45rem;border-radius:3px;margin-left:.4rem;">{{ _('Public') }}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
<h1 style="margin:0;">{{ session.label }}</h1>
|
||||
@@ -20,13 +20,13 @@
|
||||
<div style="display:flex;gap:.75rem;">
|
||||
<a href="{{ url_for('sessions.edit', session_id=session.id) }}"
|
||||
style="background:#f0f4ff;color:#1a1a2e;padding:0.5rem 1.1rem;border-radius:4px;font-size:0.9rem;text-decoration:none;">
|
||||
Edit
|
||||
{{ _('Edit') }}
|
||||
</a>
|
||||
<form method="post" action="{{ url_for('sessions.delete', session_id=session.id) }}"
|
||||
onsubmit="return confirm('Delete this session? This cannot be undone.');">
|
||||
onsubmit="return confirm('{{ _('Delete this session? This cannot be undone.') | e }}');">
|
||||
<button type="submit"
|
||||
style="background:#fff0f0;color:#c0392b;border:1px solid #f5c6c6;border-radius:4px;padding:0.5rem 1.1rem;font-size:0.9rem;cursor:pointer;">
|
||||
Delete
|
||||
{{ _('Delete') }}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
@@ -38,7 +38,7 @@
|
||||
|
||||
{% if session.location_name or session.distance_m %}
|
||||
<div style="background:#f8f9fb;border-radius:6px;padding:1rem;">
|
||||
<div style="font-size:0.75rem;text-transform:uppercase;letter-spacing:.05em;color:#888;margin-bottom:.4rem;">Location</div>
|
||||
<div style="font-size:0.75rem;text-transform:uppercase;letter-spacing:.05em;color:#888;margin-bottom:.4rem;">{{ _('Location') }}</div>
|
||||
{% if session.location_name %}<div style="font-weight:600;">{{ session.location_name }}</div>{% endif %}
|
||||
{% if session.distance_m %}<div style="color:#555;font-size:0.9rem;">{{ session.distance_m }} m</div>{% endif %}
|
||||
</div>
|
||||
@@ -46,18 +46,18 @@
|
||||
|
||||
{% if session.weather_cond or session.weather_temp_c is not none or session.weather_wind_kph is not none %}
|
||||
<div style="background:#f8f9fb;border-radius:6px;padding:1rem;">
|
||||
<div style="font-size:0.75rem;text-transform:uppercase;letter-spacing:.05em;color:#888;margin-bottom:.4rem;">Weather</div>
|
||||
<div style="font-size:0.75rem;text-transform:uppercase;letter-spacing:.05em;color:#888;margin-bottom:.4rem;">{{ _('Weather') }}</div>
|
||||
{% if session.weather_cond %}<div style="font-weight:600;">{{ session.weather_cond.replace('_',' ').title() }}</div>{% endif %}
|
||||
<div style="color:#555;font-size:0.9rem;">
|
||||
{% if session.weather_temp_c is not none %}{{ session.weather_temp_c }}°C{% endif %}
|
||||
{% if session.weather_wind_kph is not none %} {{ session.weather_wind_kph }} km/h wind{% endif %}
|
||||
{% if session.weather_wind_kph is not none %} {{ session.weather_wind_kph }} {{ _('km/h wind') }}{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if session.rifle %}
|
||||
<div style="background:#f8f9fb;border-radius:6px;padding:1rem;">
|
||||
<div style="font-size:0.75rem;text-transform:uppercase;letter-spacing:.05em;color:#888;margin-bottom:.4rem;">Rifle / Handgun</div>
|
||||
<div style="font-size:0.75rem;text-transform:uppercase;letter-spacing:.05em;color:#888;margin-bottom:.4rem;">{{ _('Rifle / Handgun') }}</div>
|
||||
<div style="font-weight:600;">{{ session.rifle.name }}</div>
|
||||
{% if session.rifle.caliber %}<div style="color:#555;font-size:0.9rem;">{{ session.rifle.caliber }}</div>{% endif %}
|
||||
</div>
|
||||
@@ -65,14 +65,14 @@
|
||||
|
||||
{% if session.scope %}
|
||||
<div style="background:#f8f9fb;border-radius:6px;padding:1rem;">
|
||||
<div style="font-size:0.75rem;text-transform:uppercase;letter-spacing:.05em;color:#888;margin-bottom:.4rem;">Scope</div>
|
||||
<div style="font-size:0.75rem;text-transform:uppercase;letter-spacing:.05em;color:#888;margin-bottom:.4rem;">{{ _('Scope') }}</div>
|
||||
<div style="font-weight:600;">{{ session.scope.name }}</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if session.ammo_brand or session.ammo_weight_gr is not none %}
|
||||
<div style="background:#f8f9fb;border-radius:6px;padding:1rem;">
|
||||
<div style="font-size:0.75rem;text-transform:uppercase;letter-spacing:.05em;color:#888;margin-bottom:.4rem;">Ammo</div>
|
||||
<div style="font-size:0.75rem;text-transform:uppercase;letter-spacing:.05em;color:#888;margin-bottom:.4rem;">{{ _('Ammunition') }}</div>
|
||||
{% if session.ammo_brand %}<div style="font-weight:600;">{{ session.ammo_brand }}</div>{% endif %}
|
||||
<div style="color:#555;font-size:0.9rem;">
|
||||
{% if session.ammo_weight_gr is not none %}{{ session.ammo_weight_gr }} gr{% endif %}
|
||||
@@ -81,16 +81,247 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if session.shooting_position %}
|
||||
<div style="background:#f8f9fb;border-radius:6px;padding:1rem;">
|
||||
<div style="font-size:0.75rem;text-transform:uppercase;letter-spacing:.05em;color:#888;margin-bottom:.4rem;">{{ _('Position') }}</div>
|
||||
<div style="font-weight:600;">{{ session.shooting_position.replace('_',' ').title() }}</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
|
||||
{% if session.notes %}
|
||||
<h2>Notes</h2>
|
||||
<h2>{{ _('Notes') }}</h2>
|
||||
<p style="color:#555;white-space:pre-wrap;">{{ session.notes }}</p>
|
||||
{% endif %}
|
||||
|
||||
{# ---- PRS Stages ---- #}
|
||||
{% if session.session_type == 'prs' %}
|
||||
<h2>{{ _('PRS Stages') }}</h2>
|
||||
|
||||
{% set stages = session.prs_stages or [] %}
|
||||
|
||||
{# Stage table (read + edit combined) #}
|
||||
<div style="overflow-x:auto;margin-bottom:1rem;">
|
||||
<table id="prs-table" style="min-width:900px;font-size:0.88rem;">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width:36px;">N°</th>
|
||||
<th>{{ _('Name') }}</th>
|
||||
<th style="width:80px;">Dist. (m)</th>
|
||||
<th style="width:75px;">{{ _('Time (s)') }}</th>
|
||||
<th style="width:110px;">Position</th>
|
||||
<th style="width:110px;">{{ _('Elevation Dope') }}</th>
|
||||
<th style="width:110px;">{{ _('Windage Dope') }}</th>
|
||||
<th style="width:80px;">{{ _('Hits/Poss.') }}</th>
|
||||
<th>{{ _('Notes') }}</th>
|
||||
{% if is_owner %}<th style="width:36px;"></th>{% endif %}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="prs-tbody">
|
||||
{% for st in stages %}
|
||||
<tr data-idx="{{ loop.index0 }}">
|
||||
<td style="text-align:center;font-weight:600;">{{ st.num or loop.index }}</td>
|
||||
<td>{{ st.name or '' }}</td>
|
||||
<td style="text-align:center;">{{ st.distance_m or '' }}</td>
|
||||
<td style="text-align:center;">{{ st.time_s or '' }}</td>
|
||||
<td>{{ st.position or '' }}</td>
|
||||
<td style="text-align:center;">{{ st.dope_elevation or '' }}</td>
|
||||
<td style="text-align:center;">{{ st.dope_windage or '' }}</td>
|
||||
<td style="text-align:center;">
|
||||
{% if st.hits is not none %}{{ st.hits }}{% endif %}{% if st.possible %}/{{ st.possible }}{% endif %}
|
||||
</td>
|
||||
<td>{{ st.notes or '' }}</td>
|
||||
{% if is_owner %}<td></td>{% endif %}
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{% if is_owner %}
|
||||
<div style="display:flex;gap:.75rem;flex-wrap:wrap;margin-bottom:1.5rem;">
|
||||
<button onclick="enterEditMode()" id="btn-edit-stages"
|
||||
style="background:#1a1a2e;color:#fff;border:none;border-radius:4px;padding:.5rem 1.1rem;font-size:0.9rem;cursor:pointer;">
|
||||
{{ _('✏ Edit stages') }}
|
||||
</button>
|
||||
<a href="{{ url_for('sessions.dope_card', session_id=session.id) }}" target="_blank"
|
||||
style="background:#27ae60;color:#fff;padding:.5rem 1.1rem;border-radius:4px;font-size:0.9rem;text-decoration:none;">
|
||||
{{ _('📄 Generate dope card (PDF)') }}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{# Hidden form used by JS to save stages #}
|
||||
<form method="post" action="{{ url_for('sessions.save_stages', session_id=session.id) }}" id="stages-form" style="display:none;">
|
||||
<input type="hidden" name="stages_json" id="stages-json-input">
|
||||
</form>
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
var PRS_POS = [
|
||||
["prone", "Couché"],
|
||||
["standing", "Debout"],
|
||||
["kneeling", "Agenouillé"],
|
||||
["sitting", "Assis"],
|
||||
["barricade", "Barricade"],
|
||||
["rooftop", "Toit"],
|
||||
["unknown", "Variable"],
|
||||
];
|
||||
|
||||
var stages = {{ (session.prs_stages or []) | tojson }};
|
||||
|
||||
function posLabel(slug) {
|
||||
var m = PRS_POS.find(function(p) { return p[0] === slug; });
|
||||
return m ? m[1] : slug;
|
||||
}
|
||||
|
||||
function posSelect(val) {
|
||||
var s = document.createElement('select');
|
||||
s.style.cssText = 'width:100%;font-size:0.85rem;padding:.2rem;border:1px solid #ccc;border-radius:3px;';
|
||||
PRS_POS.forEach(function(p) {
|
||||
var o = document.createElement('option');
|
||||
o.value = p[0]; o.textContent = p[1];
|
||||
if (p[0] === val) o.selected = true;
|
||||
s.appendChild(o);
|
||||
});
|
||||
return s;
|
||||
}
|
||||
|
||||
function inp(val, type, placeholder) {
|
||||
var i = document.createElement('input');
|
||||
i.type = type || 'text';
|
||||
i.value = val == null ? '' : val;
|
||||
if (placeholder) i.placeholder = placeholder;
|
||||
i.style.cssText = 'width:100%;font-size:0.85rem;padding:.2rem .4rem;border:1px solid #ccc;border-radius:3px;box-sizing:border-box;';
|
||||
return i;
|
||||
}
|
||||
|
||||
function collectStages() {
|
||||
var rows = document.querySelectorAll('#prs-tbody tr');
|
||||
var result = [];
|
||||
rows.forEach(function(tr, i) {
|
||||
var cells = tr.querySelectorAll('td');
|
||||
if (!cells.length) return;
|
||||
function val(idx) {
|
||||
var el = cells[idx];
|
||||
if (!el) return '';
|
||||
var input = el.querySelector('input,select,textarea');
|
||||
return input ? (input.value || '') : (el.textContent.trim() || '');
|
||||
}
|
||||
result.push({
|
||||
num: parseInt(val(0)) || i + 1,
|
||||
name: val(1),
|
||||
distance_m: parseInt(val(2)) || null,
|
||||
time_s: parseInt(val(3)) || null,
|
||||
position: val(4),
|
||||
dope_elevation: val(5),
|
||||
dope_windage: val(6),
|
||||
hits: val(7).split('/')[0] !== '' ? parseInt(val(7).split('/')[0]) : null,
|
||||
possible: val(7).split('/')[1] !== undefined ? (parseInt(val(7).split('/')[1]) || null) : null,
|
||||
notes: val(8),
|
||||
});
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
function addRow(st) {
|
||||
var tbody = document.getElementById('prs-tbody');
|
||||
var idx = tbody.rows.length;
|
||||
st = st || { num: idx + 1 };
|
||||
var tr = document.createElement('tr');
|
||||
tr.dataset.idx = idx;
|
||||
|
||||
var hitsVal = '';
|
||||
if (st.hits != null) hitsVal = st.hits;
|
||||
if (st.possible != null) hitsVal = (hitsVal !== '' ? hitsVal : '') + '/' + st.possible;
|
||||
|
||||
[
|
||||
inp(st.num, 'number'),
|
||||
inp(st.name),
|
||||
inp(st.distance_m, 'number'),
|
||||
inp(st.time_s, 'number'),
|
||||
posSelect(st.position || ''),
|
||||
inp(st.dope_elevation, 'text', 'ex : 3.5 MOA'),
|
||||
inp(st.dope_windage, 'text', 'ex : 0.5 MOA D'),
|
||||
inp(hitsVal, 'text', 'x/y'),
|
||||
inp(st.notes),
|
||||
].forEach(function(el) {
|
||||
var td = document.createElement('td');
|
||||
td.appendChild(el);
|
||||
tr.appendChild(td);
|
||||
});
|
||||
|
||||
// Remove button
|
||||
var tdDel = document.createElement('td');
|
||||
tdDel.style.textAlign = 'center';
|
||||
var btn = document.createElement('button');
|
||||
btn.type = 'button';
|
||||
btn.textContent = '✕';
|
||||
btn.style.cssText = 'background:none;border:none;color:#c0392b;cursor:pointer;font-size:1rem;padding:0 .3rem;';
|
||||
btn.onclick = function() { tr.remove(); renumber(); };
|
||||
tdDel.appendChild(btn);
|
||||
tr.appendChild(tdDel);
|
||||
|
||||
tbody.appendChild(tr);
|
||||
}
|
||||
|
||||
function renumber() {
|
||||
document.querySelectorAll('#prs-tbody tr').forEach(function(tr, i) {
|
||||
var first = tr.querySelector('td:first-child input');
|
||||
if (first && first.type === 'number') first.value = i + 1;
|
||||
});
|
||||
}
|
||||
|
||||
window.enterEditMode = function () {
|
||||
// Replace read-only rows with editable rows
|
||||
var tbody = document.getElementById('prs-tbody');
|
||||
tbody.innerHTML = '';
|
||||
stages.forEach(function(st) { addRow(st); });
|
||||
if (!stages.length) addRow(null);
|
||||
|
||||
document.getElementById('btn-edit-stages').style.display = 'none';
|
||||
|
||||
var STR_ADD_STAGE = '{{ _("+ Add stage") | e }}';
|
||||
var STR_SAVE = '{{ _("💾 Save") | e }}';
|
||||
var STR_CANCEL = '{{ _("Cancel") | e }}';
|
||||
|
||||
// Add / Save / Cancel buttons
|
||||
var bar = document.createElement('div');
|
||||
bar.id = 'edit-bar';
|
||||
bar.style.cssText = 'display:flex;gap:.6rem;flex-wrap:wrap;margin:.75rem 0;';
|
||||
bar.innerHTML =
|
||||
'<button type="button" onclick="addRow(null);renumber();" ' +
|
||||
'style="background:#f0f4ff;color:#1a1a2e;border:1px solid #c8d4f0;border-radius:4px;padding:.4rem .9rem;font-size:0.88rem;cursor:pointer;">' +
|
||||
STR_ADD_STAGE + '</button>' +
|
||||
'<button type="button" onclick="saveStages()" ' +
|
||||
'style="background:#1a1a2e;color:#fff;border:none;border-radius:4px;padding:.4rem .9rem;font-size:0.88rem;cursor:pointer;">' +
|
||||
STR_SAVE + '</button>' +
|
||||
'<button type="button" onclick="cancelEdit()" ' +
|
||||
'style="background:none;color:#666;border:none;font-size:0.88rem;cursor:pointer;padding:.4rem .5rem;">' + STR_CANCEL + '</button>';
|
||||
document.getElementById('prs-table').after(bar);
|
||||
};
|
||||
|
||||
window.saveStages = function () {
|
||||
var data = collectStages();
|
||||
document.getElementById('stages-json-input').value = JSON.stringify(data);
|
||||
document.getElementById('stages-form').submit();
|
||||
};
|
||||
|
||||
window.cancelEdit = function () {
|
||||
location.reload();
|
||||
};
|
||||
|
||||
window.addRow = addRow;
|
||||
window.renumber = renumber;
|
||||
})();
|
||||
</script>
|
||||
{% endif %}
|
||||
|
||||
{% endif %}{# end prs #}
|
||||
|
||||
{# ---- Photos ---- #}
|
||||
{% if session.photos or is_owner %}
|
||||
<h2>Photos</h2>
|
||||
<h2>{{ _('Photos') }}</h2>
|
||||
{% if session.photos %}
|
||||
<div style="display:flex;flex-wrap:wrap;gap:1rem;margin-bottom:1.5rem;">
|
||||
{% for photo in session.photos %}
|
||||
@@ -105,7 +336,7 @@
|
||||
{% if is_owner %}
|
||||
<form method="post"
|
||||
action="{{ url_for('sessions.delete_photo', session_id=session.id, photo_id=photo.id) }}"
|
||||
onsubmit="return confirm('Delete this photo?');"
|
||||
onsubmit="return confirm('{{ _('Delete this photo?') | e }}');"
|
||||
style="position:absolute;top:4px;right:4px;">
|
||||
<button type="submit"
|
||||
style="background:rgba(0,0,0,.5);color:#fff;border:none;border-radius:3px;padding:.2rem .45rem;font-size:0.8rem;cursor:pointer;line-height:1.2;">
|
||||
@@ -116,8 +347,32 @@
|
||||
</div>
|
||||
{% if photo.annotations and photo.annotations.stats %}
|
||||
{% set s = photo.annotations.stats %}
|
||||
<div style="font-size:0.78rem;background:#f0f4ff;color:#1a1a2e;padding:.2rem .45rem;border-radius:3px;margin-top:.3rem;font-weight:600;">
|
||||
{{ s.shot_count }} shots · {{ '%.2f'|format(s.group_size_moa) }} MOA ES
|
||||
{% set a = photo.annotations %}
|
||||
<div style="margin-top:.5rem;background:#f8f9fb;border:1px solid #e0e0e0;border-radius:6px;padding:.6rem .75rem;font-size:0.8rem;min-width:180px;">
|
||||
<div style="font-weight:700;color:#1a1a2e;margin-bottom:.35rem;font-size:0.82rem;">
|
||||
{{ s.shot_count }} shot{{ 's' if s.shot_count != 1 }}
|
||||
· {{ s.shooting_distance_m | int }} m
|
||||
{% if a.clean_barrel %}<span style="background:#e8f5e9;color:#27ae60;border-radius:3px;padding:.05rem .3rem;margin-left:.3rem;">{{ _('clean barrel') }}</span>{% endif %}
|
||||
</div>
|
||||
<table style="width:100%;border-collapse:collapse;margin:0;">
|
||||
<tr>
|
||||
<td style="color:#666;padding:.15rem 0;border:none;">{{ _('Group ES') }}</td>
|
||||
<td style="font-weight:600;text-align:right;padding:.15rem 0;border:none;">{{ '%.2f'|format(s.group_size_moa) }} MOA</td>
|
||||
<td style="color:#888;text-align:right;padding:.15rem 0 .15rem .4rem;border:none;">{{ '%.1f'|format(s.group_size_mm) }} mm</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="color:#666;padding:.15rem 0;border:none;">{{ _('Mean Radius') }}</td>
|
||||
<td style="font-weight:600;text-align:right;padding:.15rem 0;border:none;">{{ '%.2f'|format(s.mean_radius_moa) }} MOA</td>
|
||||
<td style="color:#888;text-align:right;padding:.15rem 0 .15rem .4rem;border:none;">{{ '%.1f'|format(s.mean_radius_mm) }} mm</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="color:#666;padding:.15rem 0;border:none;">{{ _('Centre') }}</td>
|
||||
<td colspan="2" style="text-align:right;padding:.15rem 0;border:none;">
|
||||
{{ '%.2f'|format(s.center_dist_moa) }} MOA
|
||||
<span style="color:#888;">({{ '%.1f'|format(s.center_x_mm | abs) }} mm {{ 'R' if s.center_x_mm > 0 else 'L' if s.center_x_mm < 0 else '' }}, {{ '%.1f'|format(s.center_y_mm | abs) }} mm {{ _('low') if s.center_y_mm > 0 else _('high') if s.center_y_mm < 0 else '' }})</span>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if photo.caption %}
|
||||
@@ -145,7 +400,7 @@
|
||||
background:#1a1a2e;color:#fff;border:1px solid #1a1a2e;
|
||||
{% endif %}">
|
||||
{% if photo.annotations and photo.annotations.stats %}✓{% else %}▶{% endif %}
|
||||
Measure group
|
||||
{{ _('Measure group') }}
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
@@ -159,17 +414,17 @@
|
||||
enctype="multipart/form-data"
|
||||
style="display:flex;flex-wrap:wrap;gap:.75rem;align-items:flex-end;margin-bottom:2rem;">
|
||||
<div>
|
||||
<label style="display:block;font-size:.85rem;font-weight:600;color:#444;margin-bottom:.25rem;">Add photo</label>
|
||||
<label style="display:block;font-size:.85rem;font-weight:600;color:#444;margin-bottom:.25rem;">{{ _('Add photo') }}</label>
|
||||
<input type="file" name="photo" accept="image/*" required style="font-size:0.9rem;">
|
||||
</div>
|
||||
<div>
|
||||
<label style="display:block;font-size:.85rem;font-weight:600;color:#444;margin-bottom:.25rem;">Caption (optional)</label>
|
||||
<label style="display:block;font-size:.85rem;font-weight:600;color:#444;margin-bottom:.25rem;">{{ _('Caption (optional)') }}</label>
|
||||
<input type="text" name="caption" placeholder="e.g. 300 m target"
|
||||
style="padding:.45rem .7rem;border:1px solid #ccc;border-radius:4px;font-size:0.9rem;">
|
||||
</div>
|
||||
<button type="submit"
|
||||
style="background:#1a1a2e;color:#fff;border:none;border-radius:4px;padding:.5rem 1.1rem;font-size:0.9rem;cursor:pointer;">
|
||||
Upload
|
||||
{{ _('Upload') }}
|
||||
</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
@@ -178,25 +433,161 @@
|
||||
{# ---- Analyses ---- #}
|
||||
<h2>Analyses{% if analyses %} ({{ analyses|length }}){% endif %}</h2>
|
||||
|
||||
{% if analyses %}
|
||||
<table style="margin-bottom:1.5rem;">
|
||||
<thead>
|
||||
<tr><th>Title</th><th>Date</th><th>Shots</th><th>Groups</th><th>Mean speed</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for a in analyses %}
|
||||
<tr style="cursor:pointer;" onclick="location.href='{{ url_for('analyses.detail', analysis_id=a.id) }}'">
|
||||
<td><a href="{{ url_for('analyses.detail', analysis_id=a.id) }}" style="color:inherit;text-decoration:none;">{{ a.title }}</a></td>
|
||||
<td style="color:#666;font-size:0.88rem;">{{ a.created_at.strftime('%d %b %Y') }}</td>
|
||||
<td>{{ a.shot_count }}</td>
|
||||
<td>{{ a.group_count }}</td>
|
||||
<td>{{ "%.2f"|format(a.overall_stats.mean_speed) }} m/s</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% if analyses_display %}
|
||||
{% for a, groups_display, overview_chart in analyses_display %}
|
||||
<details open style="border:1px solid #e0e0e0;border-radius:8px;margin-bottom:1.25rem;overflow:hidden;">
|
||||
<summary style="display:flex;align-items:center;gap:.75rem;padding:.85rem 1.25rem;
|
||||
background:#f8f9fb;cursor:pointer;list-style:none;flex-wrap:wrap;">
|
||||
<span style="font-weight:700;font-size:1rem;color:#1a1a2e;flex:1;">{{ a.title }}</span>
|
||||
<span style="font-size:0.82rem;color:#666;">{{ a.shot_count }} {{ _('shots') }} · {{ a.group_count }} {{ _('group') if a.group_count == 1 else _('groups') }}</span>
|
||||
<span style="font-size:0.82rem;color:#888;">{{ "%.2f"|format(a.overall_stats.mean_speed) }} {{ _('m/s mean') }}</span>
|
||||
<span style="font-size:0.78rem;color:#aaa;">{{ a.created_at.strftime('%d %b %Y') }}</span>
|
||||
</summary>
|
||||
|
||||
<div style="padding:1.25rem 1.5rem;">
|
||||
|
||||
{# --- Owner action bar: rename / standalone link / PDF / delete --- #}
|
||||
{% if is_owner %}
|
||||
<div style="display:flex;flex-wrap:wrap;gap:.6rem;align-items:center;margin-bottom:1.25rem;">
|
||||
|
||||
{# Rename inline form #}
|
||||
<details style="display:inline;">
|
||||
<summary style="display:inline-block;padding:.3rem .75rem;background:#f0f4ff;color:#1a1a2e;
|
||||
border:1px solid #c8d4f0;border-radius:4px;font-size:0.82rem;cursor:pointer;list-style:none;">
|
||||
{{ _('✏ Rename') }}
|
||||
</summary>
|
||||
<form method="post" action="{{ url_for('analyses.rename', analysis_id=a.id) }}"
|
||||
style="display:flex;gap:.5rem;align-items:center;margin-top:.5rem;">
|
||||
<input type="text" name="title" value="{{ a.title }}" required
|
||||
style="padding:.4rem .65rem;border:1px solid #ccc;border-radius:4px;font-size:0.9rem;min-width:220px;">
|
||||
<button type="submit"
|
||||
style="background:#1a1a2e;color:#fff;border:none;border-radius:4px;padding:.4rem .9rem;font-size:0.85rem;cursor:pointer;">
|
||||
{{ _('Save') }}
|
||||
</button>
|
||||
</form>
|
||||
</details>
|
||||
|
||||
<a href="{{ url_for('analyses.detail', analysis_id=a.id) }}"
|
||||
style="padding:.3rem .75rem;background:#f0f4ff;color:#1a1a2e;border:1px solid #c8d4f0;
|
||||
border-radius:4px;font-size:0.82rem;text-decoration:none;">
|
||||
{{ _('Full view') }}
|
||||
</a>
|
||||
|
||||
{% if a.pdf_path %}
|
||||
<a href="{{ url_for('analyses.download_pdf', analysis_id=a.id) }}"
|
||||
style="padding:.3rem .75rem;background:#f0f4ff;color:#1a1a2e;border:1px solid #c8d4f0;
|
||||
border-radius:4px;font-size:0.82rem;text-decoration:none;">
|
||||
↧ PDF
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
<form method="post" action="{{ url_for('analyses.delete', analysis_id=a.id) }}"
|
||||
onsubmit="return confirm('{{ _('Delete this analysis? This cannot be undone.') | e }}');"
|
||||
style="display:inline;">
|
||||
<button type="submit"
|
||||
style="background:#fff0f0;color:#c0392b;border:1px solid #f5c6c6;border-radius:4px;
|
||||
padding:.3rem .75rem;font-size:0.82rem;cursor:pointer;">
|
||||
{{ _('Delete') }}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{# --- Overview chart --- #}
|
||||
{% if overview_chart %}
|
||||
<img src="data:image/png;base64,{{ overview_chart }}" class="chart-img" alt="Overview chart"
|
||||
style="margin-bottom:1.25rem;">
|
||||
{% endif %}
|
||||
|
||||
{# --- Per-group cards --- #}
|
||||
{% if groups_display %}
|
||||
{% for gs, chart in groups_display %}
|
||||
<div class="group-section" style="margin-bottom:1rem;">
|
||||
<div class="group-meta">
|
||||
<strong>{{ _('Group %(n)s', n=loop.index) }}</strong>
|
||||
· {{ gs.count }} {{ _('shots') }}
|
||||
· {{ _('mean') }} {{ "%.2f"|format(gs.mean_speed) }} m/s
|
||||
{% if gs.std_speed is not none %} · {{ _('SD') }} {{ "%.2f"|format(gs.std_speed) }}{% endif %}
|
||||
· {{ _('ES') }} {{ "%.2f"|format(gs.max_speed - gs.min_speed) }}
|
||||
</div>
|
||||
<img src="data:image/png;base64,{{ chart }}" class="chart-img" alt="Group {{ loop.index }} chart">
|
||||
|
||||
{% if gs.note %}
|
||||
<div style="margin-top:.75rem;padding:.5rem .75rem;background:#fffbea;border-left:3px solid #f0c040;
|
||||
border-radius:0 4px 4px 0;font-size:0.88rem;color:#555;white-space:pre-wrap;">{{ gs.note }}</div>
|
||||
{% endif %}
|
||||
|
||||
{% if is_owner %}
|
||||
<details style="margin-top:.75rem;">
|
||||
<summary style="font-size:0.82rem;color:#888;cursor:pointer;list-style:none;display:inline;">
|
||||
✎ {{ _('Note') }}
|
||||
</summary>
|
||||
<form method="post"
|
||||
action="{{ url_for('analyses.save_group_note', analysis_id=a.id, group_index=loop.index0) }}"
|
||||
style="margin-top:.4rem;display:flex;flex-direction:column;gap:.4rem;">
|
||||
<textarea name="note" rows="2"
|
||||
style="padding:.45rem .65rem;border:1px solid #ccc;border-radius:4px;font-size:0.88rem;resize:vertical;width:100%;max-width:520px;">{{ gs.note or '' }}</textarea>
|
||||
<div>
|
||||
<button type="submit"
|
||||
style="background:#1a1a2e;color:#fff;border:none;border-radius:4px;padding:.35rem .85rem;font-size:0.82rem;cursor:pointer;">
|
||||
{{ _('Save note') }}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</details>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% elif groups_display is none %}
|
||||
<p style="color:#e74c3c;font-size:0.9rem;">{{ _('CSV file missing — cannot display charts.') }}</p>
|
||||
{% endif %}
|
||||
|
||||
{# --- Re-group panel (owner only) --- #}
|
||||
{% if is_owner %}
|
||||
<details style="margin-top:1rem;border-top:1px solid #e8e8e8;padding-top:.9rem;">
|
||||
<summary style="font-size:0.85rem;color:#888;cursor:pointer;list-style:none;">
|
||||
⚙ {{ _('Re-group settings') }}
|
||||
</summary>
|
||||
<form method="post" action="{{ url_for('analyses.regroup', analysis_id=a.id) }}"
|
||||
style="margin-top:.75rem;display:flex;flex-direction:column;gap:.75rem;max-width:480px;">
|
||||
<div>
|
||||
<label style="display:block;font-size:.85rem;font-weight:600;color:#444;margin-bottom:.3rem;">
|
||||
{{ _('Outlier factor:') }}
|
||||
<span id="factor_val_{{ a.id }}">{{ a.grouping_outlier_factor or 5 }}</span>
|
||||
</label>
|
||||
<input type="range" name="outlier_factor" min="1" max="20" step="0.5"
|
||||
value="{{ a.grouping_outlier_factor or 5 }}"
|
||||
style="width:100%;"
|
||||
oninput="document.getElementById('factor_val_{{ a.id }}').textContent=this.value">
|
||||
<div style="display:flex;justify-content:space-between;font-size:0.75rem;color:#aaa;">
|
||||
<span>{{ _('1 (fine)') }}</span><span>{{ _('20 (coarse)') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label style="display:block;font-size:.85rem;font-weight:600;color:#444;margin-bottom:.3rem;">
|
||||
{{ _('Manual split indices (JSON array, e.g. [5, 12])') }}
|
||||
</label>
|
||||
<input type="text" name="manual_splits"
|
||||
value="{{ (a.grouping_manual_splits | tojson) if a.grouping_manual_splits else '' }}"
|
||||
placeholder="e.g. [5, 12]"
|
||||
style="width:100%;padding:.45rem .7rem;border:1px solid #ccc;border-radius:4px;font-size:0.9rem;">
|
||||
<div style="font-size:0.75rem;color:#aaa;margin-top:.2rem;">{{ _('Shot positions (0-based) where a new group should always begin.') }}</div>
|
||||
</div>
|
||||
<div>
|
||||
<button type="submit"
|
||||
style="background:#1a1a2e;color:#fff;border:none;border-radius:4px;padding:.45rem 1.1rem;font-size:0.88rem;cursor:pointer;">
|
||||
{{ _('Apply') }}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</details>
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
</details>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<p style="color:#888;margin-bottom:1.5rem;">No analyses yet.</p>
|
||||
<p style="color:#888;margin-bottom:1.5rem;">{{ _('No analyses yet.') }}</p>
|
||||
{% endif %}
|
||||
|
||||
{% if is_owner %}
|
||||
@@ -205,12 +596,12 @@
|
||||
enctype="multipart/form-data"
|
||||
style="display:flex;flex-wrap:wrap;gap:.75rem;align-items:flex-end;margin-bottom:2rem;">
|
||||
<div>
|
||||
<label style="display:block;font-size:.85rem;font-weight:600;color:#444;margin-bottom:.25rem;">Upload chronograph CSV</label>
|
||||
<label style="display:block;font-size:.85rem;font-weight:600;color:#444;margin-bottom:.25rem;">{{ _('Upload chronograph CSV') }}</label>
|
||||
<input type="file" name="csv_file" accept=".csv,text/csv" required style="font-size:0.9rem;">
|
||||
</div>
|
||||
<button type="submit"
|
||||
style="background:#1a1a2e;color:#fff;border:none;border-radius:4px;padding:.5rem 1.1rem;font-size:0.9rem;cursor:pointer;">
|
||||
Analyse & link
|
||||
{{ _('Analyse & link') }}
|
||||
</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
|
||||
@@ -1,44 +1,93 @@
|
||||
{% extends "base.html" %}
|
||||
{% set editing = session is not none %}
|
||||
{% block title %}{{ 'Edit session' if editing else 'New session' }} — The Shooter's Network{% endblock %}
|
||||
|
||||
{# Effective type: prefill form > existing session > URL param #}
|
||||
{% set eff_type = (prefill.session_type if prefill else None) or (session.session_type if session else None) or selected_type or '' %}
|
||||
|
||||
{# Find display name for this type #}
|
||||
{% set type_name = '' %}
|
||||
{% for slug, name, _ in (session_types or []) %}{% if slug == eff_type %}{% set type_name = name %}{% endif %}{% endfor %}
|
||||
|
||||
{% block title %}{{ _('Edit session') if editing else _('New session') }} — The Shooter's Network{% endblock %}
|
||||
{% block content %}
|
||||
<h1>{{ 'Edit session' if editing else 'Log a session' }}</h1>
|
||||
|
||||
<div style="display:flex;align-items:center;gap:1rem;margin-bottom:.4rem;">
|
||||
{% if not editing %}
|
||||
<a href="{{ url_for('sessions.new') }}" style="font-size:0.85rem;color:#888;text-decoration:none;">{{ _('← Change type') }}</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<h1 style="margin-bottom:1.5rem;">
|
||||
{{ _('Edit session') if editing else _('New session') }}
|
||||
{% if type_name %}
|
||||
<span style="font-size:0.95rem;font-weight:400;color:#666;margin-left:.6rem;">— {{ type_name }}</span>
|
||||
{% endif %}
|
||||
</h1>
|
||||
|
||||
{% set f = prefill or session %}
|
||||
|
||||
<form method="post"
|
||||
action="{{ url_for('sessions.edit', session_id=session.id) if editing else url_for('sessions.new') }}"
|
||||
style="max-width:580px;">
|
||||
style="max-width:600px;">
|
||||
|
||||
<h2>Basic info</h2>
|
||||
<input type="hidden" name="session_type" id="session_type_hidden" value="{{ eff_type }}">
|
||||
|
||||
{# In edit mode: allow changing type via a small selector #}
|
||||
{% if editing %}
|
||||
<div style="margin-bottom:1.5rem;padding:.75rem 1rem;background:#f8f9fb;border-radius:6px;display:flex;align-items:center;gap:1rem;">
|
||||
<label style="font-size:.85rem;font-weight:600;color:#444;white-space:nowrap;">{{ _('Session type:') }}</label>
|
||||
<select id="type_sel" onchange="document.getElementById('session_type_hidden').value=this.value;applyType(this.value);"
|
||||
style="padding:.4rem .7rem;border:1px solid #ccc;border-radius:4px;font-size:0.9rem;background:#fff;">
|
||||
{% for slug, name, _ in (session_types or []) %}
|
||||
<option value="{{ slug }}" {% if slug == eff_type %}selected{% endif %}>{{ name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{# ── Section: basic information ── #}
|
||||
<h2>{{ _('Basic information') }}</h2>
|
||||
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:1rem;margin-bottom:1rem;">
|
||||
<div>
|
||||
<label class="fl">Date *</label>
|
||||
<label class="fl">{{ _('Date *') }}</label>
|
||||
<input type="date" name="session_date" required
|
||||
value="{{ (f.session_date.isoformat() if f.session_date else '') if f else (today or '') }}"
|
||||
style="width:100%;padding:0.55rem 0.75rem;border:1px solid #ccc;border-radius:4px;font-size:0.95rem;">
|
||||
</div>
|
||||
<div>
|
||||
<label class="fl">Distance (m)</label>
|
||||
<input type="number" name="distance_m" min="1" max="5000"
|
||||
value="{{ f.distance_m if f and f.distance_m else '' }}"
|
||||
|
||||
{# Distance: shown for long_range and pistol_25m (hidden for prs — per stage) #}
|
||||
<div id="field_distance_wrap">
|
||||
<label class="fl">{{ _('Distance (m)') }}</label>
|
||||
<input type="number" name="distance_m" min="1" max="5000" id="field_distance"
|
||||
value="{{ f.distance_m if f and f.distance_m else (prefill_distance or '') }}"
|
||||
style="width:100%;padding:0.55rem 0.75rem;border:1px solid #ccc;border-radius:4px;font-size:0.95rem;">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom:1rem;">
|
||||
<label class="fl">Location</label>
|
||||
<label class="fl">{{ _('Location') }}</label>
|
||||
<input type="text" name="location_name" value="{{ f.location_name if f else '' }}"
|
||||
placeholder="e.g. Range name, city"
|
||||
placeholder="ex : Nom du stand, commune"
|
||||
style="width:100%;padding:0.55rem 0.75rem;border:1px solid #ccc;border-radius:4px;font-size:0.95rem;">
|
||||
</div>
|
||||
|
||||
<h2>Weather</h2>
|
||||
{# ── Section: shooting position (long_range and pistol_25m) ── #}
|
||||
<div id="section_position">
|
||||
<div style="margin-bottom:1rem;">
|
||||
<label class="fl">{{ _('Shooting position') }}</label>
|
||||
<select name="shooting_position" id="field_position"
|
||||
style="width:100%;padding:0.55rem 0.75rem;border:1px solid #ccc;border-radius:4px;font-size:0.93rem;background:#fff;">
|
||||
{# Options injected by JS based on type #}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# ── Section: weather ── #}
|
||||
<h2>{{ _('Weather') }}</h2>
|
||||
<div style="display:grid;grid-template-columns:repeat(3,1fr);gap:1rem;margin-bottom:1rem;">
|
||||
<div>
|
||||
<label class="fl">Condition</label>
|
||||
<label class="fl">{{ _('Conditions') }}</label>
|
||||
<select name="weather_cond"
|
||||
style="width:100%;padding:0.55rem 0.75rem;border:1px solid #ccc;border-radius:4px;font-size:0.93rem;background:#fff;">
|
||||
{% for val, label in weather_conditions %}
|
||||
@@ -47,27 +96,27 @@
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="fl">Temp (°C)</label>
|
||||
<label class="fl">{{ _('Temp. (°C)') }}</label>
|
||||
<input type="number" name="weather_temp_c" step="0.1"
|
||||
value="{{ f.weather_temp_c if f and f.weather_temp_c is not none else '' }}"
|
||||
style="width:100%;padding:0.55rem 0.75rem;border:1px solid #ccc;border-radius:4px;font-size:0.95rem;">
|
||||
</div>
|
||||
<div>
|
||||
<label class="fl">Wind (km/h)</label>
|
||||
<label class="fl">{{ _('Wind (km/h)') }}</label>
|
||||
<input type="number" name="weather_wind_kph" step="0.1" min="0"
|
||||
value="{{ f.weather_wind_kph if f and f.weather_wind_kph is not none else '' }}"
|
||||
style="width:100%;padding:0.55rem 0.75rem;border:1px solid #ccc;border-radius:4px;font-size:0.95rem;">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2>Equipment & Ammo</h2>
|
||||
|
||||
{# ── Section: equipment & ammunition ── #}
|
||||
<h2>{{ _('Equipment & Ammunition') }}</h2>
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:1rem;margin-bottom:1rem;">
|
||||
<div>
|
||||
<label class="fl">Rifle / Handgun</label>
|
||||
<label class="fl">{{ _('Rifle / Handgun') }}</label>
|
||||
<select name="rifle_id"
|
||||
style="width:100%;padding:0.55rem 0.75rem;border:1px solid #ccc;border-radius:4px;font-size:0.93rem;background:#fff;">
|
||||
<option value="">— none —</option>
|
||||
<option value="">{{ _('— none —') }}</option>
|
||||
{% for r in rifles %}
|
||||
<option value="{{ r.id }}" {% if f and f.rifle_id == r.id %}selected{% endif %}>
|
||||
{{ r.name }}{% if r.caliber %} ({{ r.caliber }}){% endif %}
|
||||
@@ -76,15 +125,15 @@
|
||||
</select>
|
||||
{% if not rifles %}
|
||||
<div style="font-size:0.78rem;color:#aaa;margin-top:.25rem;">
|
||||
<a href="{{ url_for('equipment.new') }}">Add a rifle first</a>
|
||||
<a href="{{ url_for('equipment.new') }}">{{ _('Add a rifle first') }}</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div>
|
||||
<label class="fl">Scope</label>
|
||||
<label class="fl">{{ _('Scope') }}</label>
|
||||
<select name="scope_id"
|
||||
style="width:100%;padding:0.55rem 0.75rem;border:1px solid #ccc;border-radius:4px;font-size:0.93rem;background:#fff;">
|
||||
<option value="">— none —</option>
|
||||
<option value="">{{ _('— none —') }}</option>
|
||||
{% for sc in scopes %}
|
||||
<option value="{{ sc.id }}" {% if f and f.scope_id == sc.id %}selected{% endif %}>{{ sc.name }}</option>
|
||||
{% endfor %}
|
||||
@@ -94,50 +143,97 @@
|
||||
|
||||
<div style="display:grid;grid-template-columns:repeat(3,1fr);gap:1rem;margin-bottom:1rem;">
|
||||
<div>
|
||||
<label class="fl">Ammo brand</label>
|
||||
<label class="fl">{{ _('Ammo brand') }}</label>
|
||||
<input type="text" name="ammo_brand" value="{{ f.ammo_brand if f else '' }}"
|
||||
placeholder="e.g. Lapua, Federal"
|
||||
placeholder="ex : Lapua, RWS"
|
||||
style="width:100%;padding:0.55rem 0.75rem;border:1px solid #ccc;border-radius:4px;font-size:0.95rem;">
|
||||
</div>
|
||||
<div>
|
||||
<label class="fl">Bullet weight (gr)</label>
|
||||
<label class="fl">{{ _('Bullet weight (gr)') }}</label>
|
||||
<input type="number" name="ammo_weight_gr" step="0.1" min="0"
|
||||
value="{{ f.ammo_weight_gr if f and f.ammo_weight_gr is not none else '' }}"
|
||||
style="width:100%;padding:0.55rem 0.75rem;border:1px solid #ccc;border-radius:4px;font-size:0.95rem;">
|
||||
</div>
|
||||
<div>
|
||||
<label class="fl">Lot number</label>
|
||||
<label class="fl">{{ _('Lot number') }}</label>
|
||||
<input type="text" name="ammo_lot" value="{{ f.ammo_lot if f else '' }}"
|
||||
style="width:100%;padding:0.55rem 0.75rem;border:1px solid #ccc;border-radius:4px;font-size:0.95rem;">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2>Notes & Visibility</h2>
|
||||
|
||||
{# ── Section: notes & visibility ── #}
|
||||
<h2>{{ _('Notes & Visibility') }}</h2>
|
||||
<div style="margin-bottom:1rem;">
|
||||
<label class="fl">Notes</label>
|
||||
<label class="fl">{{ _('Notes') }}</label>
|
||||
<textarea name="notes" rows="4"
|
||||
style="width:100%;padding:0.55rem 0.75rem;border:1px solid #ccc;border-radius:4px;font-size:0.95rem;resize:vertical;">{{ f.notes if f else '' }}</textarea>
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom:1.5rem;">
|
||||
<label style="display:flex;align-items:center;gap:0.6rem;cursor:pointer;font-size:0.95rem;">
|
||||
<input type="checkbox" name="is_public" value="1"
|
||||
{% if f and f.is_public %}checked{% endif %}
|
||||
style="width:16px;height:16px;">
|
||||
Make this session public (visible in the community feed)
|
||||
{{ _('Make this session public (visible in the community feed)') }}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div style="display:flex;gap:1rem;align-items:center;">
|
||||
<button type="submit"
|
||||
style="background:#1a1a2e;color:#fff;border:none;border-radius:4px;padding:0.6rem 1.5rem;font-size:0.95rem;cursor:pointer;">
|
||||
{{ 'Save changes' if editing else 'Log session' }}
|
||||
{{ _('Save') if editing else _('Create session') }}
|
||||
</button>
|
||||
<a href="{{ url_for('sessions.detail', session_id=session.id) if editing else url_for('sessions.index') }}"
|
||||
style="font-size:0.9rem;color:#666;">Cancel</a>
|
||||
style="font-size:0.9rem;color:#666;">{{ _('Cancel') }}</a>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<style>.fl { display:block; font-size:.88rem; font-weight:600; color:#444; margin-bottom:.3rem; }</style>
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
var LR_POS = {{ (long_range_positions | tojson) if long_range_positions else '[]' }};
|
||||
var P25_POS = {{ (pistol_25m_positions | tojson) if pistol_25m_positions else '[]' }};
|
||||
|
||||
function buildOptions(sel, opts, currentVal) {
|
||||
sel.innerHTML = '';
|
||||
opts.forEach(function(o) {
|
||||
var opt = document.createElement('option');
|
||||
opt.value = o[0]; opt.textContent = o[1];
|
||||
if (o[0] === currentVal) opt.selected = true;
|
||||
sel.appendChild(opt);
|
||||
});
|
||||
}
|
||||
|
||||
var currentPosition = {{ ((f.shooting_position if f else None) or '') | tojson }};
|
||||
|
||||
function applyType(t) {
|
||||
var distWrap = document.getElementById('field_distance_wrap');
|
||||
var posSection = document.getElementById('section_position');
|
||||
var posSelect = document.getElementById('field_position');
|
||||
|
||||
if (t === 'prs') {
|
||||
if (distWrap) distWrap.style.display = 'none';
|
||||
if (posSection) posSection.style.display = 'none';
|
||||
} else if (t === 'pistol_25m') {
|
||||
if (distWrap) distWrap.style.display = '';
|
||||
if (posSection) posSection.style.display = '';
|
||||
buildOptions(posSelect, P25_POS, currentPosition);
|
||||
var distField = document.getElementById('field_distance');
|
||||
if (distField && !distField.value) distField.value = '25';
|
||||
} else {
|
||||
// long_range
|
||||
if (distWrap) distWrap.style.display = '';
|
||||
if (posSection) posSection.style.display = '';
|
||||
buildOptions(posSelect, LR_POS, currentPosition);
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
applyType({{ eff_type | tojson }});
|
||||
});
|
||||
|
||||
// Expose for the type selector in edit mode
|
||||
window.applyType = applyType;
|
||||
})();
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Sessions — The Shooter's Network{% endblock %}
|
||||
{% block title %}{{ _('Sessions') }} — The Shooter's Network{% endblock %}
|
||||
{% block content %}
|
||||
<div style="display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:1rem;margin-bottom:1.5rem;">
|
||||
<h1 style="margin:0;">My Sessions</h1>
|
||||
<h1 style="margin:0;">{{ _('My Sessions') }}</h1>
|
||||
<a href="{{ url_for('sessions.new') }}"
|
||||
style="background:#1a1a2e;color:#fff;padding:0.5rem 1.2rem;border-radius:4px;font-size:0.92rem;text-decoration:none;">
|
||||
+ New session
|
||||
{{ _('+ New session') }}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@@ -13,9 +13,10 @@
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Session</th>
|
||||
<th>Location</th>
|
||||
<th>Visibility</th>
|
||||
<th>{{ _('Session') }}</th>
|
||||
<th>{{ _('Type') }}</th>
|
||||
<th>{{ _('Location') }}</th>
|
||||
<th>{{ _('Visibility') }}</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -23,15 +24,24 @@
|
||||
{% for s in sessions %}
|
||||
<tr>
|
||||
<td><a href="{{ url_for('sessions.detail', session_id=s.id) }}">{{ s.session_date.strftime('%d %b %Y') }}</a></td>
|
||||
<td style="font-size:0.82rem;">
|
||||
{% if s.session_type == 'long_range' %}
|
||||
<span style="background:#f0f4ff;color:#1a1a2e;padding:.15rem .5rem;border-radius:3px;">{{ _('Long Range') }}</span>
|
||||
{% elif s.session_type == 'prs' %}
|
||||
<span style="background:#fff3e0;color:#e65100;padding:.15rem .5rem;border-radius:3px;">PRS</span>
|
||||
{% elif s.session_type == 'pistol_25m' %}
|
||||
<span style="background:#f3e5f5;color:#6a1b9a;padding:.15rem .5rem;border-radius:3px;">{{ _('25m Pistol') }}</span>
|
||||
{% else %}—{% endif %}
|
||||
</td>
|
||||
<td style="color:#666;font-size:0.88rem;">{{ s.location_name or '—' }}</td>
|
||||
<td style="font-size:0.85rem;color:{% if s.is_public %}#27ae60{% else %}#aaa{% endif %};">
|
||||
{{ 'Public' if s.is_public else 'Private' }}
|
||||
{{ _('Public') if s.is_public else _('Private') }}
|
||||
</td>
|
||||
<td style="white-space:nowrap;">
|
||||
<a href="{{ url_for('sessions.edit', session_id=s.id) }}" style="font-size:0.85rem;margin-right:.75rem;">Edit</a>
|
||||
<a href="{{ url_for('sessions.edit', session_id=s.id) }}" style="font-size:0.85rem;margin-right:.75rem;">{{ _('Edit') }}</a>
|
||||
<form method="post" action="{{ url_for('sessions.delete', session_id=s.id) }}" style="display:inline;"
|
||||
onsubmit="return confirm('Delete this session?');">
|
||||
<button type="submit" class="btn-link" style="font-size:0.85rem;color:#e74c3c;">Delete</button>
|
||||
onsubmit="return confirm('{{ _('Delete this session?') | e }}');">
|
||||
<button type="submit" class="btn-link" style="font-size:0.85rem;color:#e74c3c;">{{ _('Delete') }}</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -41,8 +51,8 @@
|
||||
{% else %}
|
||||
<div style="text-align:center;padding:3rem 0;color:#888;">
|
||||
<div style="font-size:3rem;margin-bottom:1rem;">🎯</div>
|
||||
<p style="margin-bottom:1rem;">No sessions recorded yet.</p>
|
||||
<a href="{{ url_for('sessions.new') }}">Log your first session</a>
|
||||
<p style="margin-bottom:1rem;">{{ _('No sessions recorded yet.') }}</p>
|
||||
<a href="{{ url_for('sessions.new') }}">{{ _('Log your first session') }}</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
40
templates/sessions/type_picker.html
Normal file
40
templates/sessions/type_picker.html
Normal file
@@ -0,0 +1,40 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}{{ _('New session') }} — The Shooter's Network{% endblock %}
|
||||
{% block content %}
|
||||
<div style="display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:1rem;margin-bottom:.5rem;">
|
||||
<h1 style="margin:0;">{{ _('New session') }}</h1>
|
||||
<a href="{{ url_for('sessions.index') }}" style="font-size:0.9rem;color:#666;">{{ _('Cancel') }}</a>
|
||||
</div>
|
||||
<p style="color:#666;margin-bottom:2rem;font-size:0.95rem;">{{ _('Choose the session type.') }}</p>
|
||||
|
||||
<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(260px,1fr));gap:1.5rem;">
|
||||
|
||||
{% for slug, name, desc in session_types %}
|
||||
<a href="{{ url_for('sessions.new', type=slug) }}"
|
||||
style="display:flex;flex-direction:column;padding:1.5rem 1.75rem;border:2px solid #e0e0e0;border-radius:10px;
|
||||
text-decoration:none;color:inherit;background:#fff;
|
||||
transition:border-color .15s,box-shadow .15s,transform .1s;"
|
||||
onmouseover="this.style.borderColor='#1a1a2e';this.style.boxShadow='0 4px 14px rgba(0,0,0,.1)';this.style.transform='translateY(-2px)'"
|
||||
onmouseout="this.style.borderColor='#e0e0e0';this.style.boxShadow='none';this.style.transform='none'">
|
||||
|
||||
{% if slug == 'long_range' %}
|
||||
<div style="font-size:2rem;margin-bottom:.6rem;">🎯</div>
|
||||
{% elif slug == 'prs' %}
|
||||
<div style="font-size:2rem;margin-bottom:.6rem;">🏔️</div>
|
||||
{% else %}
|
||||
<div style="font-size:2rem;margin-bottom:.6rem;">🔫</div>
|
||||
{% endif %}
|
||||
|
||||
<div style="font-weight:700;font-size:1.1rem;color:#1a1a2e;margin-bottom:.4rem;">{{ _(name) }}</div>
|
||||
<div style="font-size:0.85rem;color:#777;line-height:1.5;flex:1;">{{ _(desc) }}</div>
|
||||
|
||||
{% if slug == 'prs' %}
|
||||
<div style="margin-top:1rem;font-size:0.78rem;color:#1f77b4;font-weight:600;">
|
||||
{{ _('Stage management, PDF dope card ↗') }}
|
||||
</div>
|
||||
{% endif %}
|
||||
</a>
|
||||
{% endfor %}
|
||||
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -1,13 +1,13 @@
|
||||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
<h1>New Analysis</h1>
|
||||
<h1>{{ _('New Analysis') }}</h1>
|
||||
|
||||
{% if error %}
|
||||
<div class="error">{{ error }}</div>
|
||||
{% endif %}
|
||||
|
||||
<p style="margin-bottom:1.5rem; color:#555;">
|
||||
Upload a CSV file to analyse shot groups. The file must contain the following columns:
|
||||
{{ _('Upload a CSV file to analyse shot groups. The file must contain the following columns:') }}
|
||||
<strong>index</strong>, <strong>speed</strong>, <strong>standard deviation</strong>,
|
||||
<strong>energy</strong>, <strong>power factor</strong>, <strong>time of the day</strong>.
|
||||
</p>
|
||||
@@ -24,7 +24,7 @@
|
||||
type="submit"
|
||||
style="background:#1f77b4;color:#fff;border:none;border-radius:4px;padding:0.55rem 1.4rem;font-size:0.95rem;cursor:pointer;"
|
||||
>
|
||||
Analyze
|
||||
{{ _('Analyze') }}
|
||||
</button>
|
||||
</form>
|
||||
{% endblock %}
|
||||
|
||||
Reference in New Issue
Block a user