added new simple calculator feature.

This commit is contained in:
2026-06-04 17:11:26 +02:00
parent b71a685c58
commit 3269e05f6a
7 changed files with 7440 additions and 4814 deletions
+7
View File
@@ -1103,6 +1103,13 @@ def results_calculator():
translations=get_translations(),
current_language=get_current_language())
@app.route('/calculator')
def standalone_calculator():
"""Standalone calculator — no tournament required"""
return render_template('calculator.html',
translations=get_translations(),
current_language=get_current_language())
@app.route('/results')
def results_display():
"""Results display page"""
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
-318
View File
@@ -1,318 +0,0 @@
{
"rounds": [
{
"round_number": 1,
"players": [
{
"id": 14,
"name": "Karli Proje",
"enabled": true
},
{
"id": 23,
"name": "Robi Krautberger",
"enabled": true
},
{
"id": 11,
"name": "Rado Kefer",
"enabled": true
},
{
"id": 43,
"name": "Marko Pokržnik",
"enabled": true
},
{
"id": 7,
"name": "Branko Pokeržnik",
"enabled": true
},
{
"id": 9,
"name": "Janez Božič",
"enabled": true
}
],
"status": "completed"
},
{
"round_number": 2,
"players": [
{
"id": 39,
"name": "Tia Sudar",
"enabled": true
},
{
"id": 16,
"name": "Silvo Poročnik",
"enabled": true
},
{
"id": 31,
"name": "Dejan Kučnik",
"enabled": true
},
{
"id": 10,
"name": "Mitja Čeh",
"enabled": true
},
{
"id": 48,
"name": "Janja Salcman",
"enabled": true
},
{
"id": 50,
"name": "Vid Ravnjak",
"enabled": true
}
],
"status": "completed"
},
{
"round_number": 3,
"players": [
{
"id": 1,
"name": "Domen Pleterski",
"enabled": true
},
{
"id": 35,
"name": "Vanja Kolar",
"enabled": true
},
{
"id": 36,
"name": "Klara Wankmuller",
"enabled": true
},
{
"id": 46,
"name": "Tijana Štumpfl",
"enabled": true
},
{
"id": 34,
"name": "Jože Planinšec",
"enabled": true
},
{
"id": 3,
"name": "Ivan Tandler",
"enabled": true
}
],
"status": "completed"
},
{
"round_number": 4,
"players": [
{
"id": 26,
"name": "Jakob Herman",
"enabled": true
},
{
"id": 25,
"name": "Andrej Herman",
"enabled": true
},
{
"id": 30,
"name": "Maja Hirtl",
"enabled": true
},
{
"id": 4,
"name": "Mateja Pleterski",
"enabled": true
},
{
"id": 29,
"name": "Alen Kolar",
"enabled": true
},
{
"id": 5,
"name": "Jože Verhnjak",
"enabled": true
}
],
"status": "completed"
},
{
"round_number": 5,
"players": [
{
"id": 44,
"name": "Anka Kačnik",
"enabled": true
},
{
"id": 42,
"name": "Jure Glaser",
"enabled": true
},
{
"id": 45,
"name": "Lidija Blimen",
"enabled": true
},
{
"id": 32,
"name": "David Strniša",
"enabled": true
},
{
"id": 51,
"name": "Robi Ovčar",
"enabled": true
},
{
"id": 41,
"name": "Tadej Štruc",
"enabled": true
}
],
"status": "completed"
},
{
"round_number": 6,
"players": [
{
"id": 13,
"name": "Angelca Mrak",
"enabled": true
},
{
"id": 24,
"name": "Jože Verdinek",
"enabled": true
},
{
"id": 49,
"name": "Jolanda Verhnjak",
"enabled": true
},
{
"id": 18,
"name": "Matjaž Pleterski",
"enabled": true
},
{
"id": 40,
"name": "Jaka Cvar",
"enabled": true
},
{
"id": 19,
"name": "Franc Rizmal",
"enabled": true
}
],
"status": "pending"
},
{
"round_number": 7,
"players": [
{
"id": 2,
"name": "Nik Pleterski",
"enabled": true
},
{
"id": 17,
"name": "Dušan Onuk",
"enabled": true
},
{
"id": 28,
"name": "Anže Kolar",
"enabled": true
},
{
"id": 21,
"name": "Marko Blimen",
"enabled": true
},
{
"id": 20,
"name": "Jože Preglav",
"enabled": true
},
{
"id": 22,
"name": "Doris Fesel",
"enabled": true
}
],
"status": "waiting"
},
{
"round_number": 8,
"players": [
{
"id": 15,
"name": "Jan Pleterski",
"enabled": true
},
{
"id": 6,
"name": "Mateja Senica",
"enabled": true
},
{
"id": 8,
"name": "Franc Žigart",
"enabled": true
},
{
"id": 12,
"name": "Matej Kvasnik",
"enabled": true
},
{
"id": 33,
"name": "Namir Uzunović",
"enabled": true
},
{
"id": 38,
"name": "Bojan Sudar",
"enabled": true
}
],
"status": "waiting"
},
{
"round_number": 9,
"players": [
{
"id": 37,
"name": "Milan Stramec",
"enabled": true
},
{
"id": 27,
"name": "Janez Mrak",
"enabled": true
},
{
"id": 47,
"name": "Ljuba Mršak",
"enabled": true
}
],
"status": "waiting"
}
],
"created_at": "2026-04-22T17:12:17.015870",
"total_players": 51,
"total_rounds": 9,
"current_round": 6,
"tournament_type": "20_targets"
}
+503
View File
@@ -0,0 +1,503 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Kalkulator</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="/static/css/base.css">
<link rel="stylesheet" href="/static/css/navbar.css">
<link rel="stylesheet" href="/static/css/buttons.css">
<style>
html, body {
height: 100%;
margin: 0;
padding: 0;
overflow: hidden;
}
body {
display: flex;
flex-direction: column;
height: 100vh;
background: #f0f2f5;
}
.navbar { flex-shrink: 0; z-index: 100; }
/* ── PAGE WRAPPER ── */
.page {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
overflow: hidden;
min-height: 0;
}
/* ── MAIN CARD ── */
.main-card {
background: white;
border-radius: 16px;
box-shadow: 0 8px 32px rgba(0,0,0,0.12);
width: 100%;
max-width: 1100px;
max-height: 100%;
display: flex;
flex-direction: row;
overflow: hidden;
}
/* ── LEFT PANEL: config ── */
.left-panel {
width: 260px;
flex-shrink: 0;
display: flex;
flex-direction: column;
border-right: 1px solid #e9ecef;
background: #f8f9fa;
}
.left-header {
padding: 22px 24px 16px;
border-bottom: 1px solid #e9ecef;
}
.left-header h3 {
margin: 0;
font-size: 1.1rem;
font-weight: 700;
color: #2c3e50;
}
.left-body {
padding: 20px 24px;
display: flex;
flex-direction: column;
gap: 20px;
flex: 1;
}
.setup-field {
display: flex;
flex-direction: column;
gap: 8px;
}
.setup-field label {
font-size: 0.8rem;
font-weight: 700;
color: #6c757d;
text-transform: uppercase;
letter-spacing: 0.4px;
}
.setup-input {
padding: 13px 14px;
border: 2px solid #dee2e6;
border-radius: 10px;
font-size: 1.15rem;
font-weight: 600;
color: #2c3e50;
text-align: center;
background: white;
transition: border-color 0.2s;
outline: none;
width: 100%;
box-sizing: border-box;
}
.setup-input:focus {
border-color: #28a745;
box-shadow: 0 0 0 3px rgba(40,167,69,0.12);
}
.setup-hint {
font-size: 0.72rem;
color: #adb5bd;
}
.gen-btn {
background: #28a745;
color: white;
border: none;
border-radius: 10px;
padding: 14px;
font-size: 1rem;
font-weight: 700;
cursor: pointer;
transition: background 0.2s, transform 0.1s;
width: 100%;
}
.gen-btn:hover:not(:disabled) { background: #1e7e34; transform: translateY(-1px); }
.gen-btn:disabled { background: #ced4da; cursor: not-allowed; }
/* stats + actions below the fold */
.left-stats {
display: none;
flex-direction: column;
gap: 14px;
padding: 20px 24px;
border-top: 1px solid #e9ecef;
}
.left-stats.visible { display: flex; }
.stat-row {
display: flex;
justify-content: space-between;
align-items: baseline;
}
.stat-label {
font-size: 0.8rem;
font-weight: 600;
color: #6c757d;
text-transform: uppercase;
letter-spacing: 0.3px;
}
.stat-value {
font-size: 1.8rem;
font-weight: 700;
line-height: 1;
}
#totalScore { color: #28a745; }
#tensCount { color: #ffc107; }
.progress-bar-wrap {
height: 6px;
background: #e9ecef;
border-radius: 3px;
overflow: hidden;
}
.progress-bar-fill {
height: 100%;
background: #28a745;
border-radius: 3px;
transition: width 0.25s ease;
}
.left-actions {
padding: 0 24px 24px;
display: none;
flex-direction: column;
gap: 10px;
}
.left-actions.visible { display: flex; }
.action-btn {
padding: 11px 12px;
border-radius: 8px;
font-size: 0.88rem;
font-weight: 600;
cursor: pointer;
transition: all 0.15s;
width: 100%;
text-align: center;
}
.btn-zeros {
background: white;
border: 2px solid #6c757d;
color: #6c757d;
}
.btn-zeros:hover { background: #6c757d; color: white; }
.btn-reset {
background: white;
border: 2px solid #dc3545;
color: #dc3545;
}
.btn-reset:hover { background: #dc3545; color: white; }
/* ── RIGHT PANEL: grid ── */
.right-panel {
flex: 1;
display: flex;
flex-direction: column;
min-width: 0;
overflow: hidden;
}
.right-empty {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
color: #adb5bd;
gap: 10px;
font-size: 1rem;
}
.right-empty .empty-icon { font-size: 3rem; }
.targets-wrapper {
flex: 1;
overflow-y: auto;
overflow-x: hidden;
padding: 18px 20px 20px;
min-height: 0;
display: none;
}
.targets-wrapper.visible { display: block; }
.targets-wrapper::-webkit-scrollbar { width: 7px; }
.targets-wrapper::-webkit-scrollbar-track { background: transparent; }
.targets-wrapper::-webkit-scrollbar-thumb { background: #ced4da; border-radius: 4px; }
.targets-grid {
display: grid;
gap: 10px;
}
.target-group {
border: 2px solid #e9ecef;
border-radius: 8px;
background: #f8f9fa;
display: flex;
flex-direction: column;
overflow: hidden;
transition: border-color 0.15s, background 0.15s;
}
.target-group.completed {
border-color: #28a745;
background: #f0f8f5;
}
.target-num {
background: #e9ecef;
border-bottom: 1px solid #dee2e6;
font-size: 0.75rem;
font-weight: 700;
color: #495057;
text-align: center;
padding: 5px 4px;
flex-shrink: 0;
transition: background 0.15s, color 0.15s;
}
.target-group.completed .target-num {
background: linear-gradient(135deg, #e8f5e8, #c8e6c9);
border-bottom-color: #28a745;
color: #28a745;
}
.shots-col {
display: flex;
flex-direction: column;
gap: 5px;
padding: 6px;
}
.shot-input {
width: 100%;
padding: 6px 2px;
border: 1.5px solid #dee2e6;
border-radius: 4px;
text-align: center;
font-size: 1rem;
font-weight: 700;
color: #333;
background: white;
min-height: 32px;
box-sizing: border-box;
transition: border-color 0.15s, color 0.15s;
outline: none;
}
.shot-input:focus {
border-color: #28a745;
box-shadow: 0 0 0 2px rgba(40,167,69,0.2);
}
.shot-input.valid { border-color: #28a745; }
.shot-input.ten { border-color: #ffc107; color: #ffc107; }
</style>
<script src="/static/js/i18n.js"></script>
</head>
<body>
<div class="navbar">
<div class="navbar-title">🎯 Kalkulator</div>
<div class="navbar-controls">
<a href="/" class="nav-btn">✕</a>
</div>
</div>
<div class="page">
<div class="main-card">
<!-- LEFT: config + stats -->
<div class="left-panel">
<div class="left-header">
<h3>🎯 Konfiguracija</h3>
</div>
<div class="left-body">
<div class="setup-field">
<label>Število tarč</label>
<input type="number" id="inputTargets" class="setup-input" min="1" max="100" value="20" oninput="validateSetup()">
</div>
<div class="setup-field">
<label>Strelov na tarčo</label>
<input type="number" id="inputShots" class="setup-input" min="1" max="10" value="2" oninput="validateSetup()">
<span class="setup-hint">Maks. vrednost strela: 10</span>
</div>
<button class="gen-btn" id="startBtn" onclick="startCalculator()">Generiraj</button>
</div>
<div class="left-stats" id="leftStats">
<div class="stat-row">
<span class="stat-label">Točke</span>
<span class="stat-value" id="totalScore">0</span>
</div>
<div class="stat-row">
<span class="stat-label">10-ke</span>
<span class="stat-value" id="tensCount">0</span>
</div>
<div class="progress-bar-wrap">
<div class="progress-bar-fill" id="progressBar" style="width:0%"></div>
</div>
<span class="setup-hint" id="calcMeta" style="text-align:center;"></span>
</div>
<div class="left-actions" id="leftActions">
<button class="action-btn btn-zeros" onclick="fillZeros()">Zapolni 0</button>
<button class="action-btn btn-reset" onclick="resetScores()">Pobriši vse</button>
</div>
</div>
<!-- RIGHT: grid -->
<div class="right-panel">
<div class="right-empty" id="rightEmpty">
<span class="empty-icon">🎯</span>
<span>Nastavi konfiguracijo in klikni Generiraj</span>
</div>
<div class="targets-wrapper" id="targetsWrapper">
<div class="targets-grid" id="targetsGrid"></div>
</div>
</div>
</div>
</div>
<script>
let numTargets = 20;
let shotsPerTarget = 2;
let scores = {};
function validateSetup() {
const t = parseInt(document.getElementById('inputTargets').value);
const s = parseInt(document.getElementById('inputShots').value);
document.getElementById('startBtn').disabled = !(t >= 1 && t <= 100 && s >= 1 && s <= 10);
}
function startCalculator() {
numTargets = parseInt(document.getElementById('inputTargets').value);
shotsPerTarget = parseInt(document.getElementById('inputShots').value);
scores = {};
for (let t = 1; t <= numTargets; t++) {
scores[t] = {};
for (let s = 1; s <= shotsPerTarget; s++) scores[t][s] = null;
}
document.getElementById('calcMeta').textContent =
`${numTargets} × ${shotsPerTarget} = maks. ${numTargets * shotsPerTarget * 10} točk`;
buildGrid();
document.getElementById('rightEmpty').style.display = 'none';
document.getElementById('targetsWrapper').classList.add('visible');
document.getElementById('leftStats').classList.add('visible');
document.getElementById('leftActions').classList.add('visible');
updateStats();
const first = document.querySelector('.shot-input');
if (first) { first.focus(); first.select(); }
}
function buildGrid() {
const grid = document.getElementById('targetsGrid');
let cols;
if (numTargets <= 4) cols = numTargets;
else if (numTargets <= 10) cols = 5;
else if (numTargets <= 20) cols = 5;
else if (numTargets <= 40) cols = 8;
else cols = 10;
grid.style.gridTemplateColumns = `repeat(${cols}, 1fr)`;
let html = '';
for (let t = 1; t <= numTargets; t++) {
html += `<div class="target-group" id="tg-${t}">
<div class="target-num">${t}</div>
<div class="shots-col">`;
for (let s = 1; s <= shotsPerTarget; s++) {
html += `<input type="number" class="shot-input" id="si-${t}-${s}"
min="0" max="10" data-t="${t}" data-s="${s}"
oninput="onShot(${t},${s},this.value)"
onkeydown="onShotKey(event,${t},${s})">`;
}
html += `</div></div>`;
}
grid.innerHTML = html;
}
function onShot(t, s, raw) {
const input = document.getElementById(`si-${t}-${s}`);
let val = null;
if (raw !== '' && raw !== null) {
val = Math.max(0, Math.min(10, parseInt(raw) || 0));
if (input.value !== val.toString()) input.value = val;
}
scores[t][s] = val;
input.classList.remove('valid', 'ten');
if (val !== null) input.classList.add(val === 10 ? 'ten' : 'valid');
updateTargetGroup(t);
updateStats();
}
function onShotKey(e, t, s) {
if (e.key === 'Enter' || e.key === 'Tab') {
e.preventDefault();
const inputs = Array.from(document.querySelectorAll('.shot-input'));
const idx = inputs.indexOf(document.getElementById(`si-${t}-${s}`));
const next = inputs[idx + 1];
if (next) { next.focus(); next.select(); }
}
}
function updateTargetGroup(t) {
const allFilled = Object.values(scores[t]).every(v => v !== null);
document.getElementById(`tg-${t}`).classList.toggle('completed', allFilled);
}
function updateStats() {
let total = 0, tens = 0, filled = 0;
const totalSlots = numTargets * shotsPerTarget;
for (let t = 1; t <= numTargets; t++)
for (let s = 1; s <= shotsPerTarget; s++) {
const v = scores[t][s];
if (v !== null) { total += v; filled++; if (v === 10) tens++; }
}
document.getElementById('totalScore').textContent = total;
document.getElementById('tensCount').textContent = tens;
document.getElementById('progressBar').style.width = `${(filled / totalSlots) * 100}%`;
}
function fillZeros() {
for (let t = 1; t <= numTargets; t++) {
for (let s = 1; s <= shotsPerTarget; s++) {
if (scores[t][s] === null) {
scores[t][s] = 0;
const inp = document.getElementById(`si-${t}-${s}`);
inp.value = 0;
inp.classList.remove('ten');
inp.classList.add('valid');
}
}
updateTargetGroup(t);
}
updateStats();
}
function resetScores() {
if (!confirm('Pobrisati vse vnose?')) return;
for (let t = 1; t <= numTargets; t++) {
for (let s = 1; s <= shotsPerTarget; s++) {
scores[t][s] = null;
const inp = document.getElementById(`si-${t}-${s}`);
inp.value = '';
inp.classList.remove('valid', 'ten');
}
updateTargetGroup(t);
}
updateStats();
}
validateSetup();
</script>
</body>
</html>
+1
View File
@@ -803,6 +803,7 @@
<div class="settings-group">
<h4 data-i18n="league.tournaments">Turnirji</h4>
<a href="/tournament" class="nav-link" id="tournamentModeLink">🏆 <span data-i18n="tournament.tournament_mode">Način Turnirja</span></a>
<a href="/calculator" class="nav-link">🎯 Kalkulator</a>
<a href="/archive/player-analysis" class="nav-link">👤 <span data-i18n="players.player_analysis">Analiza Igralcev</span></a>
<a href="/archive" class="nav-link">📚 <span data-i18n="navigation.archive">Arhiv</span></a>
</div>