Added new calculator with embedded simple tournament

This commit is contained in:
2026-06-05 22:48:49 +02:00
parent 3269e05f6a
commit 1ad9534819
6 changed files with 842 additions and 243 deletions
+45
View File
@@ -2569,5 +2569,50 @@ def set_language(language):
'message': f'Unsupported language: {language}' 'message': f'Unsupported language: {language}'
}), 400 }), 400
# ── Calculator leaderboard & display settings ────────────────────────────────
CALC_LEADERBOARD_FILE = os.path.join('data', 'calculator_leaderboard.json')
@app.route('/api/calculator/leaderboard', methods=['GET'])
def get_calculator_leaderboard():
try:
if os.path.exists(CALC_LEADERBOARD_FILE):
with open(CALC_LEADERBOARD_FILE, 'r', encoding='utf-8') as f:
return jsonify(json.load(f))
return jsonify([])
except Exception as e:
return jsonify({'status': 'error', 'message': str(e)}), 500
@app.route('/api/calculator/leaderboard', methods=['POST'])
def save_calculator_leaderboard():
try:
data = request.get_json()
os.makedirs('data', exist_ok=True)
with open(CALC_LEADERBOARD_FILE, 'w', encoding='utf-8') as f:
json.dump(data, f, indent=2, ensure_ascii=False)
return jsonify({'status': 'success'})
except Exception as e:
return jsonify({'status': 'error', 'message': str(e)}), 500
@app.route('/api/calculator/display', methods=['GET'])
def get_calculator_display():
try:
settings = load_settings()
return jsonify(settings.get('calc_display', {'enabled': False, 'camera': 1}))
except Exception as e:
return jsonify({'status': 'error', 'message': str(e)}), 500
@app.route('/api/calculator/display', methods=['POST'])
def save_calculator_display():
try:
data = request.get_json()
settings = load_settings()
settings['calc_display'] = {'enabled': bool(data.get('enabled')), 'camera': int(data.get('camera', 1))}
save_settings(settings)
return jsonify({'status': 'success'})
except Exception as e:
return jsonify({'status': 'error', 'message': str(e)}), 500
if __name__ == '__main__': if __name__ == '__main__':
app.run(host='0.0.0.0', port=5000, debug=True) app.run(host='0.0.0.0', port=5000, debug=True)
+5 -1
View File
@@ -35,7 +35,11 @@ DEFAULT_SETTINGS = {
'show_titles': True, 'show_titles': True,
'title_size': 1.1 'title_size': 1.1
}, },
'disabled_cameras': [] 'disabled_cameras': [],
'calc_display': {
'enabled': False,
'camera': 1
}
} }
+1
View File
@@ -0,0 +1 @@
[]
+5 -1
View File
@@ -13,5 +13,9 @@
"target_number_size": 1.7 "target_number_size": 1.7
}, },
"tv_view": "cameras", "tv_view": "cameras",
"disabled_cameras": [] "disabled_cameras": [],
"calc_display": {
"enabled": true,
"camera": 4
}
} }
+594 -240
View File
@@ -1,5 +1,5 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="sl">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<title>Kalkulator</title> <title>Kalkulator</title>
@@ -8,39 +8,24 @@
<link rel="stylesheet" href="/static/css/navbar.css"> <link rel="stylesheet" href="/static/css/navbar.css">
<link rel="stylesheet" href="/static/css/buttons.css"> <link rel="stylesheet" href="/static/css/buttons.css">
<style> <style>
html, body { body { display: flex; flex-direction: column; height: 100vh; overflow: hidden; background: #f5f5f5; }
height: 100%; .navbar { flex-shrink: 0; }
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 { .page {
flex: 1; flex: 1;
display: flex; display: flex;
align-items: center; align-items: stretch;
justify-content: center; justify-content: center;
padding: 20px; padding: 16px;
overflow: hidden; overflow: hidden;
min-height: 0; min-height: 0;
} }
/* ── MAIN CARD ── */
.main-card { .main-card {
background: white; background: white;
border-radius: 16px; border-radius: 16px;
box-shadow: 0 8px 32px rgba(0,0,0,0.12); box-shadow: 0 8px 32px rgba(0,0,0,0.12);
width: 100%; width: 100%;
max-width: 1100px; max-width: 1400px;
max-height: 100%;
display: flex; display: flex;
flex-direction: row; flex-direction: row;
overflow: hidden; overflow: hidden;
@@ -48,50 +33,27 @@
/* ── LEFT PANEL: config ── */ /* ── LEFT PANEL: config ── */
.left-panel { .left-panel {
width: 260px; width: 240px;
flex-shrink: 0; flex-shrink: 0;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
border-right: 1px solid #e9ecef; border-right: 1px solid #e9ecef;
background: #f8f9fa; background: #f8f9fa;
} }
.left-header { height: 65px; padding: 0 16px; border-bottom: 1px solid #e9ecef; flex-shrink: 0; display: flex; align-items: center; }
.left-header h3 { margin: 0; font-size: 1.05rem; font-weight: 700; color: #2c3e50; line-height: 1; }
.left-body { padding: 16px 20px; display: flex; flex-direction: column; gap: 14px; flex: 1; }
.left-header { .setup-field { display: flex; flex-direction: column; gap: 6px; }
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 { .setup-field label {
font-size: 0.8rem; font-size: 0.75rem; font-weight: 700; color: #6c757d;
font-weight: 700; text-transform: uppercase; letter-spacing: 0.4px;
color: #6c757d;
text-transform: uppercase;
letter-spacing: 0.4px;
} }
.setup-input { .setup-input {
padding: 13px 14px; padding: 11px 12px;
border: 2px solid #dee2e6; border: 2px solid #dee2e6;
border-radius: 10px; border-radius: 8px;
font-size: 1.15rem; font-size: 1.1rem;
font-weight: 600; font-weight: 600;
color: #2c3e50; color: #2c3e50;
text-align: center; text-align: center;
@@ -99,201 +61,373 @@
transition: border-color 0.2s; transition: border-color 0.2s;
outline: none; outline: none;
width: 100%; 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;
} }
.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 { .gen-btn {
background: #28a745; background: #28a745; color: white; border: none;
color: white; border-radius: 8px; padding: 13px;
border: none; font-size: 0.95rem; font-weight: 700; cursor: pointer;
border-radius: 10px; transition: background 0.2s, transform 0.1s; width: 100%;
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:hover:not(:disabled) { background: #1e7e34; transform: translateY(-1px); }
.gen-btn:disabled { background: #ced4da; cursor: not-allowed; } .gen-btn:disabled { background: #ced4da; cursor: not-allowed; }
/* stats + actions below the fold */ .left-utils { padding: 0 20px 18px; display: none; flex-direction: column; gap: 7px; flex-shrink: 0; }
.left-stats { .left-utils.visible { display: flex; }
display: none; .util-btn {
flex-direction: column; padding: 9px 12px; border-radius: 8px; font-size: 0.82rem;
gap: 14px; font-weight: 600; cursor: pointer; transition: all 0.15s; width: 100%; text-align: center;
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 { background: white; border: 2px solid #6c757d; color: #6c757d; }
.btn-zeros:hover { background: #6c757d; color: white; } .btn-zeros:hover { background: #6c757d; color: white; }
.btn-reset { .btn-reset { background: white; border: 2px solid #dc3545; color: #dc3545; }
background: white;
border: 2px solid #dc3545;
color: #dc3545;
}
.btn-reset:hover { background: #dc3545; color: white; } .btn-reset:hover { background: #dc3545; color: white; }
.btn-print { background: white; border: 2px solid #6f42c1; color: #6f42c1; }
.btn-print:hover { background: #6f42c1; color: white; }
/* ── RIGHT PANEL: grid ── */ @media print {
.right-panel { /* hide everything except the print layout */
.navbar, .left-panel, .middle-panel, .score-panel,
.page, .main-card, body > *:not(#printLayout) { display: none !important; }
body {
background: white !important;
overflow: visible !important;
height: auto !important;
margin: 0;
padding: 0;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Arial, sans-serif;
}
#printLayout {
display: block !important;
padding: 24px 32px;
color: #333;
}
.pl-header {
display: flex;
align-items: center;
gap: 18px;
padding-bottom: 14px;
border-bottom: 2px solid #dee2e6;
margin-bottom: 18px;
}
.pl-logo {
height: 52px;
width: auto;
object-fit: contain;
}
.pl-titles { display: flex; flex-direction: column; gap: 2px; }
.pl-title {
font-size: 20pt;
font-weight: 800;
color: #2c3e50;
line-height: 1.1;
}
.pl-subtitle {
font-size: 10pt;
color: #6c757d;
}
.pl-meta {
margin-left: auto;
text-align: right;
font-size: 9pt;
color: #6c757d;
line-height: 1.6;
}
.pl-table {
width: 100%;
border-collapse: collapse;
font-size: 11pt;
}
.pl-table thead tr {
background: #f8f9fa;
border-bottom: 2px solid #dee2e6;
}
.pl-table th {
padding: 8px 12px;
text-align: left;
font-size: 8pt;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.4px;
color: #495057;
}
.pl-table th.right { text-align: right; }
.pl-table td {
padding: 9px 12px;
border-bottom: 1px solid #f1f3f4;
color: #333;
}
.pl-table td.right { text-align: right; }
.pl-table tbody tr:nth-child(1) td { background: #fffdf0; }
.pl-table tbody tr:nth-child(2) td { background: #f9f9f9; }
.pl-table tbody tr:nth-child(3) td { background: #fdf7f2; }
.pl-rank { font-weight: 700; font-size: 12pt; }
.pl-rank.r1 { color: #b8860b; }
.pl-rank.r2 { color: #6c757d; }
.pl-rank.r3 { color: #8b4513; }
.pl-name { font-weight: 600; }
.pl-score { font-weight: 800; font-size: 13pt; color: #28a745; }
.pl-tens { font-weight: 700; color: #856404; }
}
/* display settings */
.display-section {
padding: 14px 20px 18px;
border-top: 1px solid #e9ecef;
display: flex;
flex-direction: column;
gap: 12px;
flex-shrink: 0;
}
.display-section-title {
font-size: 0.75rem; font-weight: 700; color: #6c757d;
text-transform: uppercase; letter-spacing: 0.4px;
}
.display-toggle-row {
display: flex; align-items: center; justify-content: space-between;
}
.display-toggle-label { font-size: 0.88rem; font-weight: 600; color: #2c3e50; }
.toggle-switch { position: relative; width: 42px; height: 24px; flex-shrink: 0; }
.toggle-switch input { opacity: 0; width: 0; height: 0; }
.toggle-track {
position: absolute; inset: 0; border-radius: 12px;
background: #ced4da; cursor: pointer; transition: background 0.2s;
}
.toggle-track::after {
content: ''; position: absolute; width: 18px; height: 18px;
border-radius: 50%; background: white; top: 3px; left: 3px;
transition: transform 0.2s; box-shadow: 0 1px 3px rgba(0,0,0,0.2);
}
.toggle-switch input:checked + .toggle-track { background: #28a745; }
.toggle-switch input:checked + .toggle-track::after { transform: translateX(18px); }
.display-camera-row { display: flex; flex-direction: column; gap: 6px; }
.display-camera-row label { font-size: 0.75rem; font-weight: 700; color: #6c757d; text-transform: uppercase; letter-spacing: 0.4px; }
.camera-select {
padding: 9px 12px; border: 2px solid #dee2e6; border-radius: 8px;
font-size: 0.9rem; font-weight: 600; color: #2c3e50; background: white;
outline: none; cursor: pointer; transition: border-color 0.2s; width: 100%;
}
.camera-select:focus { border-color: #28a745; }
.camera-select:disabled { opacity: 0.45; cursor: not-allowed; }
/* ── MIDDLE PANEL ── */
.middle-panel {
flex: 1; flex: 1;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
min-width: 0; min-width: 0;
overflow: hidden; overflow: hidden;
border-right: 1px solid #e9ecef;
} }
.right-empty { /* top: name inputs */
flex: 1; .name-bar {
height: 65px;
display: flex; display: flex;
flex-direction: column;
align-items: center; align-items: center;
justify-content: center;
color: #adb5bd;
gap: 10px; gap: 10px;
font-size: 1rem; padding: 0 16px;
border-bottom: 1px solid #e9ecef;
background: #f8f9fa;
flex-shrink: 0;
} }
.right-empty .empty-icon { font-size: 3rem; } .name-bar-label {
font-size: 0.75rem; font-weight: 700; color: #6c757d;
text-transform: uppercase; letter-spacing: 0.4px;
white-space: nowrap;
}
.name-input {
flex: 1;
padding: 9px 12px;
border: 2px solid #dee2e6;
border-radius: 8px;
font-size: 0.95rem;
font-weight: 500;
color: #2c3e50;
background: white;
transition: border-color 0.2s;
outline: none;
min-width: 0;
}
.name-input::placeholder { color: #adb5bd; font-weight: 400; }
.name-input:focus { border-color: #28a745; box-shadow: 0 0 0 3px rgba(40,167,69,0.12); }
/* target grid */
.middle-empty {
flex: 1; display: flex; flex-direction: column;
align-items: center; justify-content: center;
color: #adb5bd; gap: 10px; font-size: 1rem;
}
.middle-empty .empty-icon { font-size: 3rem; }
.targets-wrapper { .targets-wrapper {
flex: 1; flex: 1; overflow-y: auto; overflow-x: hidden;
overflow-y: auto; padding: 14px 16px; min-height: 0; display: none;
overflow-x: hidden;
padding: 18px 20px 20px;
min-height: 0;
display: none;
} }
.targets-wrapper.visible { display: block; } .targets-wrapper.visible { display: block; }
.targets-wrapper::-webkit-scrollbar { width: 7px; } .targets-wrapper::-webkit-scrollbar { width: 6px; }
.targets-wrapper::-webkit-scrollbar-track { background: transparent; } .targets-wrapper::-webkit-scrollbar-track { background: transparent; }
.targets-wrapper::-webkit-scrollbar-thumb { background: #ced4da; border-radius: 4px; } .targets-wrapper::-webkit-scrollbar-thumb { background: #ced4da; border-radius: 4px; }
.targets-grid { .targets-grid { display: grid; gap: 8px; }
display: grid;
gap: 10px;
}
.target-group { .target-group {
border: 2px solid #e9ecef; border: 2px solid #e9ecef; border-radius: 8px; background: #f8f9fa;
border-radius: 8px; display: flex; flex-direction: column; overflow: hidden;
background: #f8f9fa;
display: flex;
flex-direction: column;
overflow: hidden;
transition: border-color 0.15s, background 0.15s; transition: border-color 0.15s, background 0.15s;
} }
.target-group.completed { .target-group.completed { border-color: #28a745; background: #f0f8f5; }
border-color: #28a745;
background: #f0f8f5;
}
.target-num { .target-num {
background: #e9ecef; background: #e9ecef; border-bottom: 1px solid #dee2e6;
border-bottom: 1px solid #dee2e6; font-size: 0.72rem; font-weight: 700; color: #495057;
font-size: 0.75rem; text-align: center; padding: 4px; flex-shrink: 0;
font-weight: 700;
color: #495057;
text-align: center;
padding: 5px 4px;
flex-shrink: 0;
transition: background 0.15s, color 0.15s; transition: background 0.15s, color 0.15s;
} }
.target-group.completed .target-num { .target-group.completed .target-num {
background: linear-gradient(135deg, #e8f5e8, #c8e6c9); background: linear-gradient(135deg, #e8f5e8, #c8e6c9);
border-bottom-color: #28a745; border-bottom-color: #28a745; color: #28a745;
color: #28a745;
} }
.shots-col { .shots-col { display: flex; flex-direction: column; gap: 4px; padding: 5px; }
.shot-input {
width: 100%; padding: 5px 2px; border: 1.5px solid #dee2e6;
border-radius: 4px; text-align: center; font-size: 0.95rem;
font-weight: 700; color: #333; background: white;
min-height: 30px; 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: #e6a800; }
/* bottom: add bar */
.add-bar {
display: none;
align-items: center;
gap: 12px;
padding: 12px 16px;
border-top: 2px solid #e9ecef;
background: #f8f9fa;
flex-shrink: 0;
}
.add-bar.visible { display: flex; }
.add-bar-score { display: flex; flex-direction: column; align-items: center; line-height: 1.1; flex-shrink: 0; }
.add-bar-pts { font-size: 1.8rem; font-weight: 800; color: #28a745; line-height: 1; }
.add-bar-tens { font-size: 0.7rem; font-weight: 700; color: #ffc107; }
.add-bar-meta { flex: 1; min-width: 0; }
.add-bar-meta-text { font-size: 0.8rem; color: #6c757d; }
.add-bar-progress { height: 4px; background: #e9ecef; border-radius: 2px; overflow: hidden; margin-top: 4px; }
.add-bar-progress-fill { height: 100%; background: #28a745; border-radius: 2px; transition: width 0.25s ease; }
.btn-add {
background: #007bff; border: 2px solid #007bff; color: white;
padding: 11px 20px; border-radius: 8px; font-size: 0.9rem;
font-weight: 700; cursor: pointer; transition: all 0.15s;
white-space: nowrap; flex-shrink: 0;
}
.btn-add:hover:not(:disabled) { background: #0056b3; border-color: #0056b3; }
.btn-add:disabled { opacity: 0.4; cursor: not-allowed; }
/* ── RIGHT PANEL: scoreboard ── */
.score-panel {
width: 290px;
flex-shrink: 0;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 5px; background: #f8f9fa;
padding: 6px; overflow: hidden;
} }
.shot-input { .score-header {
width: 100%; height: 65px;
padding: 6px 2px; padding: 0 16px;
border: 1.5px solid #dee2e6; border-bottom: 1px solid #e9ecef;
border-radius: 4px; display: flex;
text-align: center; align-items: center;
font-size: 1rem; justify-content: space-between;
font-weight: 700; flex-shrink: 0;
color: #333;
background: white;
min-height: 32px;
box-sizing: border-box;
transition: border-color 0.15s, color 0.15s;
outline: none;
} }
.shot-input:focus { .score-header h3 { margin: 0; font-size: 1.05rem; font-weight: 700; color: #2c3e50; }
border-color: #28a745; .score-count {
box-shadow: 0 0 0 2px rgba(40,167,69,0.2); background: #28a745; color: white;
font-size: 0.72rem; font-weight: 700;
padding: 2px 8px; border-radius: 12px;
} }
.shot-input.valid { border-color: #28a745; } .score-empty {
.shot-input.ten { border-color: #ffc107; color: #ffc107; } flex: 1; display: flex; flex-direction: column;
align-items: center; justify-content: center;
color: #adb5bd; gap: 8px; font-size: 0.88rem;
padding: 20px; text-align: center;
}
.score-empty .empty-icon { font-size: 2.4rem; }
.score-list {
flex: 1; overflow-y: auto;
padding: 10px; display: flex; flex-direction: column; gap: 6px;
}
.score-list::-webkit-scrollbar { width: 5px; }
.score-list::-webkit-scrollbar-track { background: transparent; }
.score-list::-webkit-scrollbar-thumb { background: #ced4da; border-radius: 4px; }
.score-row {
display: flex; align-items: center; gap: 7px;
padding: 8px 10px;
background: white; border: 2px solid #e9ecef; border-radius: 10px;
animation: slideIn 0.2s ease;
}
@keyframes slideIn {
from { opacity: 0; transform: translateX(10px); }
to { opacity: 1; transform: translateX(0); }
}
.score-row.rank-1 { border-color: #ffc107; background: #fffdf0; }
.score-row.rank-2 { border-color: #adb5bd; background: #f9f9f9; }
.score-row.rank-3 { border-color: #cd7f32; background: #fdf7f2; }
.rank-badge {
width: 26px; height: 26px; border-radius: 50%;
display: flex; align-items: center; justify-content: center;
font-size: 0.72rem; font-weight: 800; flex-shrink: 0;
background: #e9ecef; color: #495057;
}
.score-row.rank-1 .rank-badge { background: #ffc107; color: #7a5800; }
.score-row.rank-2 .rank-badge { background: #adb5bd; color: #343a40; }
.score-row.rank-3 .rank-badge { background: #cd7f32; color: white; }
.score-name {
flex: 1; min-width: 0;
font-size: 0.88rem; font-weight: 600; color: #2c3e50;
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.score-tens { font-size: 0.7rem; font-weight: 700; color: #ffc107; flex-shrink: 0; white-space: nowrap; }
.score-pts { font-size: 1.05rem; font-weight: 800; color: #28a745; flex-shrink: 0; }
.score-del {
background: none; border: none; cursor: pointer;
color: #ced4da; font-size: 0.95rem; padding: 2px 4px;
border-radius: 4px; transition: color 0.15s, background 0.15s;
flex-shrink: 0; line-height: 1;
}
.score-del:hover { color: #dc3545; background: #fff0f0; }
.score-footer { padding: 10px 10px 0; border-top: 1px solid #e9ecef; flex-shrink: 0; }
.score-print-footer { padding: 8px 10px 10px; flex-shrink: 0; }
.btn-print {
width: 100%; padding: 9px; border-radius: 8px;
border: 2px solid #6f42c1; background: white;
color: #6f42c1; font-size: 0.82rem; font-weight: 600;
cursor: pointer; transition: all 0.15s;
}
.btn-print:hover { background: #6f42c1; color: white; }
.btn-clear-board {
width: 100%; padding: 9px; border-radius: 8px;
border: 2px solid #dc3545; background: white;
color: #dc3545; font-size: 0.82rem; font-weight: 600;
cursor: pointer; transition: all 0.15s;
}
.btn-clear-board:hover { background: #dc3545; color: white; }
</style> </style>
<script src="/static/js/i18n.js"></script> <script src="/static/js/i18n.js"></script>
</head> </head>
@@ -308,12 +442,11 @@
<div class="page"> <div class="page">
<div class="main-card"> <div class="main-card">
<!-- LEFT: config + stats --> <!-- ── LEFT: config + stats + utils ── -->
<div class="left-panel"> <div class="left-panel">
<div class="left-header"> <div class="left-header">
<h3>🎯 Konfiguracija</h3> <h3>⚙️ Nastavitve</h3>
</div> </div>
<div class="left-body"> <div class="left-body">
<div class="setup-field"> <div class="setup-field">
<label>Število tarč</label> <label>Število tarč</label>
@@ -327,45 +460,156 @@
<button class="gen-btn" id="startBtn" onclick="startCalculator()">Generiraj</button> <button class="gen-btn" id="startBtn" onclick="startCalculator()">Generiraj</button>
</div> </div>
<div class="left-stats" id="leftStats"> <div class="left-utils" id="leftUtils">
<div class="stat-row"> <button class="util-btn btn-zeros" onclick="fillZeros()">Zapolni 0</button>
<span class="stat-label">Točke</span> <button class="util-btn btn-reset" onclick="resetScores()">Pobriši vse</button>
<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>
<div class="left-actions" id="leftActions"> <div class="display-section">
<button class="action-btn btn-zeros" onclick="fillZeros()">Zapolni 0</button> <span class="display-section-title">📺 Prikaz na zaslonu</span>
<button class="action-btn btn-reset" onclick="resetScores()">Pobriši vse</button> <div class="display-toggle-row">
<span class="display-toggle-label">Omogoči prikaz</span>
<label class="toggle-switch">
<input type="checkbox" id="displayEnabled" onchange="saveDisplaySettings()">
<span class="toggle-track"></span>
</label>
</div>
<div class="display-camera-row">
<label for="displayCamera">Kamera</label>
<select id="displayCamera" class="camera-select" onchange="saveDisplaySettings()" disabled>
<option value="1">Kamera 1</option>
<option value="2">Kamera 2</option>
<option value="3">Kamera 3</option>
<option value="4">Kamera 4</option>
<option value="5">Kamera 5</option>
<option value="6">Kamera 6</option>
</select>
</div>
</div> </div>
</div> </div>
<!-- RIGHT: grid --> <!-- ── MIDDLE: name bar → targets → add bar ── -->
<div class="right-panel"> <div class="middle-panel">
<div class="right-empty" id="rightEmpty">
<div class="name-bar">
<span class="name-bar-label">👤</span>
<input type="text" id="inputName" class="name-input" placeholder="Ime in priimek" autocomplete="off">
</div>
<div class="middle-empty" id="middleEmpty">
<span class="empty-icon">🎯</span> <span class="empty-icon">🎯</span>
<span>Nastavi konfiguracijo in klikni Generiraj</span> <span>Nastavi konfiguracijo in klikni Generiraj</span>
</div> </div>
<div class="targets-wrapper" id="targetsWrapper"> <div class="targets-wrapper" id="targetsWrapper">
<div class="targets-grid" id="targetsGrid"></div> <div class="targets-grid" id="targetsGrid"></div>
</div> </div>
<div class="add-bar" id="addBar">
<div class="add-bar-score">
<span class="add-bar-pts" id="addBarPts">0</span>
<span class="add-bar-tens" id="addBarTens">0 × 10</span>
</div>
<div class="add-bar-meta">
<div class="add-bar-meta-text" id="addBarMeta"></div>
<div class="add-bar-progress">
<div class="add-bar-progress-fill" id="addBarProgress" style="width:0%"></div>
</div>
</div>
<button class="btn-add" id="addBtn" onclick="addToScoreboard()" disabled>+ Dodaj na lestvico</button>
</div>
</div>
<!-- ── RIGHT: scoreboard ── -->
<div class="score-panel">
<div class="score-header">
<h3>🏆 Lestvica</h3>
<span class="score-count" id="scoreCount">0</span>
</div>
<div class="score-empty" id="scoreEmpty">
<span class="empty-icon">🏅</span>
<span>Dodaj tekmovalce z gumbom<br><em>Dodaj na lestvico</em></span>
</div>
<div class="score-list" id="scoreList" style="display:none;"></div>
<div class="score-footer" id="scoreFooter" style="display:none;">
<button class="btn-clear-board" onclick="clearScoreboard()">Počisti lestvico</button>
</div>
<div class="score-print-footer">
<button class="btn-print" onclick="printLeaderboard()">🖨️ Natisni lestvico</button>
</div>
</div> </div>
</div> </div>
</div> </div>
<!-- Print-only layout — hidden on screen, shown on print -->
<div id="printLayout" style="display:none;">
<div class="pl-header">
<img src="/static/logo.png" class="pl-logo" alt="Logo" onerror="this.style.display='none'">
<div class="pl-titles">
<div class="pl-title">🏆 Lestvica</div>
<div class="pl-subtitle">Rezultati kalkulatorja</div>
</div>
<div class="pl-meta" id="plMeta"></div>
</div>
<table class="pl-table">
<thead>
<tr>
<th style="width:50px;">Mesto</th>
<th>Tekmovalec</th>
<th class="right" style="width:80px;">Točke</th>
<th class="right" style="width:70px;">10-ke</th>
</tr>
</thead>
<tbody id="plBody"></tbody>
</table>
</div>
<script> <script>
const STORAGE_KEY = 'kalkulator_lestvica';
let numTargets = 20; let numTargets = 20;
let shotsPerTarget = 2; let shotsPerTarget = 2;
let scores = {}; let scores = {};
let scoreboard = JSON.parse(localStorage.getItem(STORAGE_KEY) || '[]');
function saveScoreboard() {
localStorage.setItem(STORAGE_KEY, JSON.stringify(scoreboard));
fetch('/api/calculator/leaderboard', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(scoreboard)
}).catch(() => {});
}
// ── Display settings ───────────────────────────────────────────────────
function saveDisplaySettings() {
const enabled = document.getElementById('displayEnabled').checked;
const camera = parseInt(document.getElementById('displayCamera').value);
document.getElementById('displayCamera').disabled = !enabled;
fetch('/api/calculator/display', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ enabled, camera })
}).catch(() => {});
}
function loadDisplaySettings() {
fetch('/api/calculator/display')
.then(r => r.json())
.then(data => {
const enabled = !!data.enabled;
document.getElementById('displayEnabled').checked = enabled;
document.getElementById('displayCamera').value = data.camera || 1;
document.getElementById('displayCamera').disabled = !enabled;
}).catch(() => {});
}
function validateSetup() { function validateSetup() {
const t = parseInt(document.getElementById('inputTargets').value); const t = parseInt(document.getElementById('inputTargets').value);
@@ -383,15 +627,15 @@
for (let s = 1; s <= shotsPerTarget; s++) scores[t][s] = null; for (let s = 1; s <= shotsPerTarget; s++) scores[t][s] = null;
} }
document.getElementById('calcMeta').textContent = document.getElementById('addBarMeta').textContent =
`${numTargets} × ${shotsPerTarget} = maks. ${numTargets * shotsPerTarget * 10} točk`; `${numTargets} tarč × ${shotsPerTarget} strel`;
buildGrid(); buildGrid();
document.getElementById('rightEmpty').style.display = 'none'; document.getElementById('middleEmpty').style.display = 'none';
document.getElementById('targetsWrapper').classList.add('visible'); document.getElementById('targetsWrapper').classList.add('visible');
document.getElementById('leftStats').classList.add('visible'); document.getElementById('leftUtils').classList.add('visible');
document.getElementById('leftActions').classList.add('visible'); document.getElementById('addBar').classList.add('visible');
updateStats(); updateStats();
@@ -462,9 +706,90 @@
const v = scores[t][s]; const v = scores[t][s];
if (v !== null) { total += v; filled++; if (v === 10) tens++; } if (v !== null) { total += v; filled++; if (v === 10) tens++; }
} }
document.getElementById('totalScore').textContent = total;
document.getElementById('tensCount').textContent = tens; const pct = totalSlots ? (filled / totalSlots) * 100 : 0;
document.getElementById('progressBar').style.width = `${(filled / totalSlots) * 100}%`;
document.getElementById('addBarPts').textContent = total;
document.getElementById('addBarTens').textContent = `${tens} × 10`;
document.getElementById('addBarProgress').style.width = `${pct}%`;
document.getElementById('addBtn').disabled = (filled < totalSlots);
}
function addToScoreboard() {
const name = document.getElementById('inputName').value.trim() || 'Neznan';
let total = 0, tens = 0;
for (let t = 1; t <= numTargets; t++)
for (let s = 1; s <= shotsPerTarget; s++) {
const v = scores[t][s];
if (v !== null) { total += v; if (v === 10) tens++; }
}
const existing = scoreboard.find(e => e.name.toLowerCase() === name.toLowerCase());
if (existing) {
if (total > existing.score || (total === existing.score && tens > existing.tens)) {
existing.score = total;
existing.tens = tens;
}
} else {
scoreboard.push({ name, score: total, tens, id: Date.now() });
}
scoreboard.sort((a, b) => b.score - a.score || b.tens - a.tens);
saveScoreboard();
renderScoreboard();
document.getElementById('inputName').value = '';
resetScoresQuiet();
document.getElementById('inputName').focus();
}
function removeFromScoreboard(id) {
scoreboard = scoreboard.filter(e => e.id !== id);
saveScoreboard();
renderScoreboard();
}
function clearScoreboard() {
if (!confirm('Počistiti celotno lestvico?')) return;
scoreboard = [];
saveScoreboard();
renderScoreboard();
}
function renderScoreboard() {
const list = document.getElementById('scoreList');
const empty = document.getElementById('scoreEmpty');
const footer = document.getElementById('scoreFooter');
const counter = document.getElementById('scoreCount');
counter.textContent = scoreboard.length;
if (scoreboard.length === 0) {
empty.style.display = '';
list.style.display = 'none';
footer.style.display = 'none';
return;
}
empty.style.display = 'none';
list.style.display = '';
footer.style.display = '';
list.innerHTML = scoreboard.map((entry, idx) => {
const rank = idx + 1;
const rankClass = rank <= 3 ? `rank-${rank}` : '';
const medal = rank === 1 ? '🥇' : rank === 2 ? '🥈' : rank === 3 ? '🥉' : rank;
return `
<div class="score-row ${rankClass}">
<div class="rank-badge">${medal}</div>
<div class="score-name" title="${entry.name}">${entry.name}</div>
<div class="score-tens">${entry.tens}×10</div>
<div class="score-pts">${entry.score}</div>
<button class="score-del" onclick="removeFromScoreboard(${entry.id})" title="Odstrani">✕</button>
</div>`;
}).join('');
} }
function fillZeros() { function fillZeros() {
@@ -483,21 +808,50 @@
updateStats(); updateStats();
} }
function resetScores() { function resetScoresQuiet() {
if (!confirm('Pobrisati vse vnose?')) return;
for (let t = 1; t <= numTargets; t++) { for (let t = 1; t <= numTargets; t++) {
for (let s = 1; s <= shotsPerTarget; s++) { for (let s = 1; s <= shotsPerTarget; s++) {
scores[t][s] = null; scores[t][s] = null;
const inp = document.getElementById(`si-${t}-${s}`); const inp = document.getElementById(`si-${t}-${s}`);
inp.value = ''; if (inp) { inp.value = ''; inp.classList.remove('valid', 'ten'); }
inp.classList.remove('valid', 'ten');
} }
updateTargetGroup(t); const tg = document.getElementById(`tg-${t}`);
if (tg) tg.classList.remove('completed');
} }
updateStats(); updateStats();
} }
function resetScores() {
if (!confirm('Pobrisati vse vnose?')) return;
resetScoresQuiet();
}
function printLeaderboard() {
const medals = ['🥇','🥈','🥉'];
const rows = scoreboard.map((e, idx) => {
const rank = idx + 1;
const rc = rank <= 3 ? `r${rank}` : '';
const med = rank <= 3 ? medals[rank - 1] : rank;
return `<tr>
<td><span class="pl-rank ${rc}">${med}</span></td>
<td class="pl-name">${e.name}</td>
<td class="right pl-score">${e.score}</td>
<td class="right pl-tens">${e.tens}</td>
</tr>`;
}).join('');
document.getElementById('plBody').innerHTML = rows || '<tr><td colspan="4" style="text-align:center;color:#aaa;padding:20px;">Ni rezultatov</td></tr>';
const now = new Date();
document.getElementById('plMeta').innerHTML =
`${now.toLocaleDateString('sl-SI')}<br>${now.toLocaleTimeString('sl-SI', {hour:'2-digit',minute:'2-digit'})}<br>${scoreboard.length} tekmovalcev`;
window.print();
}
validateSetup(); validateSetup();
renderScoreboard();
loadDisplaySettings();
</script> </script>
</body> </body>
</html> </html>
+192 -1
View File
@@ -130,6 +130,81 @@
align-content: stretch; align-content: stretch;
} }
/* ── Leaderboard display mode ── */
.main-wrapper {
display: flex;
height: calc(100vh - 50px);
}
.main-wrapper .grid {
flex: 1;
height: 100%;
grid-template-columns: 1fr;
grid-template-rows: 1fr;
min-width: 0;
}
.lb-sidebar {
width: 320px;
flex-shrink: 0;
display: flex;
flex-direction: column;
background: #141414;
border-left: 2px solid #222;
overflow: hidden;
}
.lb-header {
height: 60px;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 20px;
border-bottom: 1px solid #222;
flex-shrink: 0;
}
.lb-title { font-size: 1.1rem; font-weight: 800; color: #fff; }
.lb-count {
background: #28a745; color: white;
font-size: 0.72rem; font-weight: 700;
padding: 2px 9px; border-radius: 12px;
}
.lb-empty {
flex: 1; display: flex; flex-direction: column;
align-items: center; justify-content: center;
color: #444; gap: 10px; font-size: 0.9rem;
}
.lb-empty .icon { font-size: 2.5rem; }
.lb-list {
flex: 1; overflow-y: auto;
padding: 10px 12px;
display: flex; flex-direction: column; gap: 7px;
}
.lb-list::-webkit-scrollbar { width: 4px; }
.lb-list::-webkit-scrollbar-thumb { background: #333; border-radius: 4px; }
.lb-row {
display: flex; align-items: center; gap: 9px;
padding: 9px 12px;
border-radius: 10px;
background: #1e1e1e; border: 2px solid #2a2a2a;
}
.lb-row.r1 { border-color: #ffc107; background: #1c1900; }
.lb-row.r2 { border-color: #888; background: #181818; }
.lb-row.r3 { border-color: #a0522d; background: #1a1210; }
.lb-badge {
width: 30px; height: 30px; border-radius: 50%;
display: flex; align-items: center; justify-content: center;
font-size: 0.78rem; font-weight: 800; flex-shrink: 0;
background: #2a2a2a; color: #aaa;
}
.lb-row.r1 .lb-badge { background: #ffc107; color: #7a5800; font-size: 1rem; }
.lb-row.r2 .lb-badge { background: #888; color: #fff; font-size: 1rem; }
.lb-row.r3 .lb-badge { background: #a0522d; color: #fff; font-size: 1rem; }
.lb-name {
flex: 1; min-width: 0;
font-size: 0.95rem; font-weight: 700; color: #eee;
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.lb-tens { font-size: 0.7rem; font-weight: 700; color: #ffc107; flex-shrink: 0; }
.lb-pts { font-size: 1.2rem; font-weight: 900; color: #28a745; flex-shrink: 0; min-width: 36px; text-align: right; }
.camera-card { .camera-card {
overflow: hidden; overflow: hidden;
display: flex; display: flex;
@@ -884,7 +959,8 @@
</div> </div>
</div> </div>
<div class="grid"> <div class="main-wrapper" id="mainWrapper">
<div class="grid" id="cameraGrid">
{% for i in range(1, 7) %} {% for i in range(1, 7) %}
<div class="camera-card" data-camera-id="{{ i }}" <div class="camera-card" data-camera-id="{{ i }}"
{% if i in settings.get('disabled_cameras', []) %}style="display:none;"{% endif %}> {% if i in settings.get('disabled_cameras', []) %}style="display:none;"{% endif %}>
@@ -916,6 +992,17 @@
{% endfor %} {% endfor %}
</div> </div>
<!-- Leaderboard sidebar — injected by JS when calc display is enabled -->
<div class="lb-sidebar" id="lbSidebar" style="display:none;">
<div class="lb-header">
<span class="lb-title">🏆 Lestvica</span>
<span class="lb-count" id="lbCount">0</span>
</div>
<div class="lb-empty" id="lbEmpty"><span class="icon">🏅</span><span>Čakamo na rezultate…</span></div>
<div class="lb-list" id="lbList" style="display:none;"></div>
</div>
</div>
<script> <script>
// Global settings and tournament state // Global settings and tournament state
let currentSettings = {{ settings|tojson }}; let currentSettings = {{ settings|tojson }};
@@ -1618,6 +1705,9 @@
window.addEventListener('resize', reshapeGrid); window.addEventListener('resize', reshapeGrid);
addTouchFeedback(); addTouchFeedback();
// Apply leaderboard display mode if enabled
initLeaderboardDisplay();
// Initialize language selector after i18n is ready // Initialize language selector after i18n is ready
window.addEventListener('i18nReady', function() { window.addEventListener('i18nReady', function() {
createLanguageSelector('languageSelectorContainer'); createLanguageSelector('languageSelectorContainer');
@@ -1698,6 +1788,107 @@
clearInterval(dashboardPollingInterval); clearInterval(dashboardPollingInterval);
} }
}); });
// ── Calculator leaderboard display mode ────────────────────────────────
let lbPollingInterval = null;
let lbDisplayEnabled = false;
function initLeaderboardDisplay() {
fetch('/api/calculator/display')
.then(r => r.json())
.then(d => applyLeaderboardMode(d.enabled, d.camera || 1))
.catch(() => {});
}
function applyLeaderboardMode(enabled, camera) {
lbDisplayEnabled = enabled;
const sidebar = document.getElementById('lbSidebar');
const grid = document.getElementById('cameraGrid');
if (!enabled) {
sidebar.style.display = 'none';
// restore camera visibility to match settings
for (let i = 1; i <= 6; i++) {
const card = document.querySelector(`.camera-card[data-camera-id="${i}"]`);
if (!card) continue;
const isDisabled = currentSettings.disabled_cameras && currentSettings.disabled_cameras.includes(i);
card.style.display = isDisabled ? 'none' : '';
}
grid.style.gridTemplateColumns = '';
if (lbPollingInterval) { clearInterval(lbPollingInterval); lbPollingInterval = null; }
reshapeGrid();
return;
}
// Show only selected camera
for (let i = 1; i <= 6; i++) {
const card = document.querySelector(`.camera-card[data-camera-id="${i}"]`);
if (!card) continue;
card.style.display = (i === camera) ? '' : 'none';
}
grid.style.gridTemplateColumns = '1fr';
grid.style.gridTemplateRows = '1fr';
sidebar.style.display = '';
reshapeGrid();
// Poll leaderboard
pollLeaderboard();
if (!lbPollingInterval) {
lbPollingInterval = setInterval(pollLeaderboard, 5000);
}
}
function pollLeaderboard() {
fetch('/api/calculator/leaderboard')
.then(r => r.json())
.then(data => { if (Array.isArray(data)) renderLbSidebar(data); })
.catch(() => {});
}
function renderLbSidebar(data) {
const list = document.getElementById('lbList');
const empty = document.getElementById('lbEmpty');
const count = document.getElementById('lbCount');
count.textContent = data.length;
if (data.length === 0) {
empty.style.display = '';
list.style.display = 'none';
return;
}
empty.style.display = 'none';
list.style.display = '';
list.innerHTML = data.map((e, idx) => {
const rank = idx + 1;
const rc = rank <= 3 ? `r${rank}` : '';
const med = rank === 1 ? '🥇' : rank === 2 ? '🥈' : rank === 3 ? '🥉' : rank;
return `<div class="lb-row ${rc}">
<div class="lb-badge">${med}</div>
<div class="lb-name" title="${e.name}">${e.name}</div>
<div class="lb-tens">${e.tens}×10</div>
<div class="lb-pts">${e.score}</div>
</div>`;
}).join('');
}
// Poll display setting every 5s so enabling/disabling from calculator
// takes effect on the main screen automatically
setInterval(() => {
fetch('/api/calculator/display')
.then(r => r.json())
.then(d => {
const enabled = !!d.enabled;
const camera = d.camera || 1;
if (enabled !== lbDisplayEnabled) {
applyLeaderboardMode(enabled, camera);
}
})
.catch(() => {});
}, 5000);
</script> </script>
</body> </body>
</html> </html>