Spaces:
Sleeping
Sleeping
Commit ·
47370ee
1
Parent(s): 857d022
Admin panel imprvmnts
Browse files- app.py +33 -11
- templates/admin.html +15 -2
- templates/base.html +59 -6
- templates/dashboard.html +33 -31
- templates/identify.html +85 -16
- templates/leaderboard.html +81 -13
- users.json +0 -1
app.py
CHANGED
|
@@ -11,6 +11,7 @@ import threading
|
|
| 11 |
from datetime import datetime, date, timedelta
|
| 12 |
from functools import wraps
|
| 13 |
from collections import defaultdict
|
|
|
|
| 14 |
|
| 15 |
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
|
| 16 |
_log = logging.getLogger(__name__)
|
|
@@ -19,6 +20,12 @@ app = Flask(__name__, template_folder=os.path.join(BASE_DIR, 'templates'))
|
|
| 19 |
app.secret_key = os.environ.get('SECRET_KEY', 'ipl-predictions-secret-change-in-prod')
|
| 20 |
app.config['PERMANENT_SESSION_LIFETIME'] = timedelta(days=90)
|
| 21 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 22 |
|
| 23 |
def get_data_dir() -> str:
|
| 24 |
"""Writable directory for SQLite. Optional DIS_DATA_DIR (e.g. /data) if you use HF paid persistent disk."""
|
|
@@ -263,8 +270,8 @@ ABBR_TO_FULL = {v: k for k, v in TEAM_ABBR.items()}
|
|
| 263 |
|
| 264 |
TEAM_COLORS = {
|
| 265 |
'MI': '#004BA0', 'CSK': '#FFCC00', 'RCB': '#EC1C24',
|
| 266 |
-
'KKR': '#
|
| 267 |
-
'RR': '#EA1A85', 'PBKS': '#AA4545', 'LSG': '#A4C639', 'GT': '#
|
| 268 |
}
|
| 269 |
|
| 270 |
POINTS_CONFIG = {
|
|
@@ -611,6 +618,9 @@ def require_login(f):
|
|
| 611 |
@wraps(f)
|
| 612 |
def wrapper(*args, **kwargs):
|
| 613 |
if not get_current_user():
|
|
|
|
|
|
|
|
|
|
| 614 |
flash('Pick your name to continue.', 'info')
|
| 615 |
return redirect(url_for('index'))
|
| 616 |
return f(*args, **kwargs)
|
|
@@ -637,17 +647,30 @@ def match_calendar_date(match) -> date:
|
|
| 637 |
|
| 638 |
|
| 639 |
def is_match_today(match) -> bool:
|
| 640 |
-
return match_calendar_date(match) ==
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 641 |
|
| 642 |
|
| 643 |
def is_prediction_locked(match) -> bool:
|
| 644 |
if match['status'] not in ('upcoming',):
|
| 645 |
return True
|
| 646 |
try:
|
| 647 |
-
|
| 648 |
-
match_dt = datetime.strptime(match_dt_str, '%Y-%m-%d %H:%M')
|
| 649 |
lock_dt = match_dt - timedelta(minutes=POINTS_CONFIG['lock_minutes_before'])
|
| 650 |
-
return
|
| 651 |
except Exception:
|
| 652 |
return True
|
| 653 |
|
|
@@ -669,10 +692,9 @@ def auto_lock_matches():
|
|
| 669 |
).fetchall()
|
| 670 |
for row in rows:
|
| 671 |
try:
|
| 672 |
-
|
| 673 |
-
match_dt = datetime.strptime(match_dt_str, '%Y-%m-%d %H:%M')
|
| 674 |
lock_dt = match_dt - timedelta(minutes=POINTS_CONFIG['lock_minutes_before'])
|
| 675 |
-
if
|
| 676 |
conn.execute(
|
| 677 |
"UPDATE matches SET status='locked', updated_at=CURRENT_TIMESTAMP WHERE id=?",
|
| 678 |
(row['id'],)
|
|
@@ -986,7 +1008,7 @@ def dashboard():
|
|
| 986 |
user = get_current_user()
|
| 987 |
conn = get_db()
|
| 988 |
|
| 989 |
-
today =
|
| 990 |
todays_matches = conn.execute(
|
| 991 |
'SELECT * FROM matches WHERE match_date=? ORDER BY match_time', (today,)
|
| 992 |
).fetchall()
|
|
@@ -1972,7 +1994,7 @@ def inject_globals():
|
|
| 1972 |
'current_user': get_current_user(),
|
| 1973 |
'team_abbr': TEAM_ABBR,
|
| 1974 |
'team_colors': TEAM_COLORS,
|
| 1975 |
-
'today':
|
| 1976 |
'points_config': POINTS_CONFIG,
|
| 1977 |
'staff_session': bool(session.get('staff_ok')),
|
| 1978 |
'admin_login_configured': bool(admin_password()),
|
|
|
|
| 11 |
from datetime import datetime, date, timedelta
|
| 12 |
from functools import wraps
|
| 13 |
from collections import defaultdict
|
| 14 |
+
from zoneinfo import ZoneInfo
|
| 15 |
|
| 16 |
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
|
| 17 |
_log = logging.getLogger(__name__)
|
|
|
|
| 20 |
app.secret_key = os.environ.get('SECRET_KEY', 'ipl-predictions-secret-change-in-prod')
|
| 21 |
app.config['PERMANENT_SESSION_LIFETIME'] = timedelta(days=90)
|
| 22 |
|
| 23 |
+
APP_TIMEZONE = (os.environ.get('APP_TIMEZONE') or 'Asia/Kolkata').strip()
|
| 24 |
+
try:
|
| 25 |
+
APP_TZ = ZoneInfo(APP_TIMEZONE)
|
| 26 |
+
except Exception:
|
| 27 |
+
APP_TZ = ZoneInfo('Asia/Kolkata')
|
| 28 |
+
|
| 29 |
|
| 30 |
def get_data_dir() -> str:
|
| 31 |
"""Writable directory for SQLite. Optional DIS_DATA_DIR (e.g. /data) if you use HF paid persistent disk."""
|
|
|
|
| 270 |
|
| 271 |
TEAM_COLORS = {
|
| 272 |
'MI': '#004BA0', 'CSK': '#FFCC00', 'RCB': '#EC1C24',
|
| 273 |
+
'KKR': '#7C3AED', 'SRH': '#FF822A', 'DC': '#0078BC',
|
| 274 |
+
'RR': '#EA1A85', 'PBKS': '#AA4545', 'LSG': '#A4C639', 'GT': '#00B5E2',
|
| 275 |
}
|
| 276 |
|
| 277 |
POINTS_CONFIG = {
|
|
|
|
| 618 |
@wraps(f)
|
| 619 |
def wrapper(*args, **kwargs):
|
| 620 |
if not get_current_user():
|
| 621 |
+
# Allow admin-only flows with staff session even if no team user is selected.
|
| 622 |
+
if session.get('staff_ok') and request.endpoint and request.endpoint.startswith('admin'):
|
| 623 |
+
return f(*args, **kwargs)
|
| 624 |
flash('Pick your name to continue.', 'info')
|
| 625 |
return redirect(url_for('index'))
|
| 626 |
return f(*args, **kwargs)
|
|
|
|
| 647 |
|
| 648 |
|
| 649 |
def is_match_today(match) -> bool:
|
| 650 |
+
return match_calendar_date(match) == app_today()
|
| 651 |
+
|
| 652 |
+
|
| 653 |
+
def app_now() -> datetime:
|
| 654 |
+
return datetime.now(APP_TZ)
|
| 655 |
+
|
| 656 |
+
|
| 657 |
+
def app_today() -> date:
|
| 658 |
+
return app_now().date()
|
| 659 |
+
|
| 660 |
+
|
| 661 |
+
def match_start_dt(match) -> datetime:
|
| 662 |
+
match_dt_str = f"{match['match_date']} {match['match_time']}"
|
| 663 |
+
naive = datetime.strptime(match_dt_str, '%Y-%m-%d %H:%M')
|
| 664 |
+
return naive.replace(tzinfo=APP_TZ)
|
| 665 |
|
| 666 |
|
| 667 |
def is_prediction_locked(match) -> bool:
|
| 668 |
if match['status'] not in ('upcoming',):
|
| 669 |
return True
|
| 670 |
try:
|
| 671 |
+
match_dt = match_start_dt(match)
|
|
|
|
| 672 |
lock_dt = match_dt - timedelta(minutes=POINTS_CONFIG['lock_minutes_before'])
|
| 673 |
+
return app_now() >= lock_dt
|
| 674 |
except Exception:
|
| 675 |
return True
|
| 676 |
|
|
|
|
| 692 |
).fetchall()
|
| 693 |
for row in rows:
|
| 694 |
try:
|
| 695 |
+
match_dt = match_start_dt(row)
|
|
|
|
| 696 |
lock_dt = match_dt - timedelta(minutes=POINTS_CONFIG['lock_minutes_before'])
|
| 697 |
+
if app_now() >= lock_dt:
|
| 698 |
conn.execute(
|
| 699 |
"UPDATE matches SET status='locked', updated_at=CURRENT_TIMESTAMP WHERE id=?",
|
| 700 |
(row['id'],)
|
|
|
|
| 1008 |
user = get_current_user()
|
| 1009 |
conn = get_db()
|
| 1010 |
|
| 1011 |
+
today = app_today().isoformat()
|
| 1012 |
todays_matches = conn.execute(
|
| 1013 |
'SELECT * FROM matches WHERE match_date=? ORDER BY match_time', (today,)
|
| 1014 |
).fetchall()
|
|
|
|
| 1994 |
'current_user': get_current_user(),
|
| 1995 |
'team_abbr': TEAM_ABBR,
|
| 1996 |
'team_colors': TEAM_COLORS,
|
| 1997 |
+
'today': app_today().isoformat(),
|
| 1998 |
'points_config': POINTS_CONFIG,
|
| 1999 |
'staff_session': bool(session.get('staff_ok')),
|
| 2000 |
'admin_login_configured': bool(admin_password()),
|
templates/admin.html
CHANGED
|
@@ -1,5 +1,18 @@
|
|
| 1 |
{% extends 'base.html' %}
|
| 2 |
{% block title %}Admin – {{ app_brand }}{% endblock %}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3 |
{% block content %}
|
| 4 |
<div class="page">
|
| 5 |
<div class="page-header" style="display:flex; flex-wrap:wrap; align-items:flex-start; justify-content:space-between; gap:1rem;">
|
|
@@ -7,7 +20,7 @@
|
|
| 7 |
<div class="page-title">⚙️ ADMIN PANEL</div>
|
| 8 |
<div class="page-subtitle">Manage matches, users, results and points</div>
|
| 9 |
</div>
|
| 10 |
-
<a href="{{ url_for('admin_logout') }}" class="btn btn-ghost btn-sm" style="align-self:center;">🔒 Log out admin</a>
|
| 11 |
</div>
|
| 12 |
|
| 13 |
<!-- Tab Nav -->
|
|
@@ -173,7 +186,7 @@
|
|
| 173 |
</div>
|
| 174 |
{% endif %}
|
| 175 |
</div>
|
| 176 |
-
<form method="post" action="{{ url_for('admin_set_result', match_id=match.id) }}" style="display:flex; flex-direction:column; gap:0.75rem; min-width:320px;">
|
| 177 |
<div class="form-row cols-2">
|
| 178 |
<div class="form-group" style="margin:0;">
|
| 179 |
<label>Winner *</label>
|
|
|
|
| 1 |
{% extends 'base.html' %}
|
| 2 |
{% block title %}Admin – {{ app_brand }}{% endblock %}
|
| 3 |
+
{% block head %}
|
| 4 |
+
<style>
|
| 5 |
+
@media (max-width: 860px) {
|
| 6 |
+
.admin-result-form {
|
| 7 |
+
min-width: 0 !important;
|
| 8 |
+
width: 100%;
|
| 9 |
+
}
|
| 10 |
+
.admin-header-logout {
|
| 11 |
+
width: 100%;
|
| 12 |
+
}
|
| 13 |
+
}
|
| 14 |
+
</style>
|
| 15 |
+
{% endblock %}
|
| 16 |
{% block content %}
|
| 17 |
<div class="page">
|
| 18 |
<div class="page-header" style="display:flex; flex-wrap:wrap; align-items:flex-start; justify-content:space-between; gap:1rem;">
|
|
|
|
| 20 |
<div class="page-title">⚙️ ADMIN PANEL</div>
|
| 21 |
<div class="page-subtitle">Manage matches, users, results and points</div>
|
| 22 |
</div>
|
| 23 |
+
<a href="{{ url_for('admin_logout') }}" class="btn btn-ghost btn-sm admin-header-logout" style="align-self:center;">🔒 Log out admin</a>
|
| 24 |
</div>
|
| 25 |
|
| 26 |
<!-- Tab Nav -->
|
|
|
|
| 186 |
</div>
|
| 187 |
{% endif %}
|
| 188 |
</div>
|
| 189 |
+
<form method="post" action="{{ url_for('admin_set_result', match_id=match.id) }}" class="admin-result-form" style="display:flex; flex-direction:column; gap:0.75rem; min-width:320px;">
|
| 190 |
<div class="form-row cols-2">
|
| 191 |
<div class="form-group" style="margin:0;">
|
| 192 |
<label>Winner *</label>
|
templates/base.html
CHANGED
|
@@ -37,6 +37,7 @@
|
|
| 37 |
color: var(--text);
|
| 38 |
min-height: 100vh;
|
| 39 |
line-height: 1.6;
|
|
|
|
| 40 |
}
|
| 41 |
|
| 42 |
/* ── SCROLLBAR ──────────────────────────────── */
|
|
@@ -127,6 +128,7 @@
|
|
| 127 |
border-radius: var(--radius-sm); border: none;
|
| 128 |
cursor: pointer; text-decoration: none;
|
| 129 |
transition: all 0.2s; white-space: nowrap;
|
|
|
|
| 130 |
}
|
| 131 |
.btn-primary { background: var(--orange); color: var(--bg); }
|
| 132 |
.btn-primary:hover { background: var(--orange2); transform: translateY(-1px); }
|
|
@@ -193,7 +195,11 @@
|
|
| 193 |
@keyframes pulse { 0%,100%{opacity:1} 50%{opacity:0.4} }
|
| 194 |
|
| 195 |
/* ── TABLES ──────────────────────────────────── */
|
| 196 |
-
.table-wrap {
|
|
|
|
|
|
|
|
|
|
|
|
|
| 197 |
table { width: 100%; border-collapse: collapse; font-size: 0.875rem; }
|
| 198 |
th {
|
| 199 |
text-align: left; padding: 0.75rem 1rem;
|
|
@@ -294,15 +300,58 @@
|
|
| 294 |
}
|
| 295 |
|
| 296 |
/* ── RESPONSIVE ──────────────────────────────── */
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 297 |
@media (max-width: 768px) {
|
| 298 |
.grid-2, .grid-3, .grid-4 { grid-template-columns: 1fr; }
|
| 299 |
.form-row.cols-2, .form-row.cols-3 { grid-template-columns: 1fr; }
|
|
|
|
|
|
|
|
|
|
| 300 |
.nav-links { display: none; flex-direction: column; position: absolute; top: 60px; left: 0; right: 0;
|
| 301 |
-
background: var(--bg2); padding:
|
|
|
|
|
|
|
| 302 |
.nav-links.open { display: flex; }
|
| 303 |
.hamburger { display: block; margin-left: auto; }
|
| 304 |
-
.
|
| 305 |
-
.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 306 |
}
|
| 307 |
|
| 308 |
/* ── ACCENT GLOW ─────────────────────────────── */
|
|
@@ -318,12 +367,12 @@
|
|
| 318 |
<body>
|
| 319 |
<div class="glow"></div>
|
| 320 |
|
| 321 |
-
{% if current_user %}
|
| 322 |
<nav>
|
| 323 |
<div class="nav-inner">
|
| 324 |
<a href="{{ url_for('index') }}" class="nav-brand" title="{{ app_brand }} — pick name / switch user">DIS <span class="brand-ipl">IPL</span> <span class="brand-year">2026</span></a>
|
| 325 |
<button class="hamburger" onclick="toggleNav()">☰</button>
|
| 326 |
<div class="nav-links" id="navLinks">
|
|
|
|
| 327 |
<a href="{{ url_for('dashboard') }}" class="nav-link {% if request.endpoint == 'dashboard' %}active{% endif %}">🏠 Home</a>
|
| 328 |
<a href="{{ url_for('user_guide') }}" class="nav-link {% if request.endpoint == 'user_guide' %}active{% endif %}">📘 Guide</a>
|
| 329 |
<a href="{{ url_for('matches') }}" class="nav-link {% if request.endpoint == 'matches' %}active{% endif %}">📅 Matches</a>
|
|
@@ -331,6 +380,9 @@
|
|
| 331 |
<a href="{{ url_for('leaderboard') }}"class="nav-link {% if request.endpoint == 'leaderboard'%}active{% endif %}">🏆 Board</a>
|
| 332 |
<a href="{{ url_for('analytics') }}" class="nav-link {% if request.endpoint == 'analytics' %}active{% endif %}">📈 Analytics</a>
|
| 333 |
<a href="{{ url_for('history') }}" class="nav-link {% if request.endpoint == 'history' %}active{% endif %}">📊 My Stats</a>
|
|
|
|
|
|
|
|
|
|
| 334 |
{% if admin_login_configured %}
|
| 335 |
{% if staff_session %}
|
| 336 |
<a href="{{ url_for('admin') }}" class="nav-link {% if request.endpoint and request.endpoint.startswith('admin') %}active{% endif %}">⚙️ Admin</a>
|
|
@@ -338,11 +390,12 @@
|
|
| 338 |
<a href="{{ url_for('admin_login', next=url_for('admin')) }}" class="nav-link">🔐 Admin</a>
|
| 339 |
{% endif %}
|
| 340 |
{% endif %}
|
|
|
|
| 341 |
<span class="pts-badge">{{ '%.0f'|format(current_user.points) }} pts</span>
|
|
|
|
| 342 |
</div>
|
| 343 |
</div>
|
| 344 |
</nav>
|
| 345 |
-
{% endif %}
|
| 346 |
|
| 347 |
<div class="alerts">
|
| 348 |
{% for cat, msg in get_flashed_messages(with_categories=True) %}
|
|
|
|
| 37 |
color: var(--text);
|
| 38 |
min-height: 100vh;
|
| 39 |
line-height: 1.6;
|
| 40 |
+
overflow-x: hidden;
|
| 41 |
}
|
| 42 |
|
| 43 |
/* ── SCROLLBAR ──────────────────────────────── */
|
|
|
|
| 128 |
border-radius: var(--radius-sm); border: none;
|
| 129 |
cursor: pointer; text-decoration: none;
|
| 130 |
transition: all 0.2s; white-space: nowrap;
|
| 131 |
+
justify-content: center;
|
| 132 |
}
|
| 133 |
.btn-primary { background: var(--orange); color: var(--bg); }
|
| 134 |
.btn-primary:hover { background: var(--orange2); transform: translateY(-1px); }
|
|
|
|
| 195 |
@keyframes pulse { 0%,100%{opacity:1} 50%{opacity:0.4} }
|
| 196 |
|
| 197 |
/* ── TABLES ──────────────────────────────────── */
|
| 198 |
+
.table-wrap {
|
| 199 |
+
overflow-x: auto;
|
| 200 |
+
-webkit-overflow-scrolling: touch;
|
| 201 |
+
scrollbar-width: thin;
|
| 202 |
+
}
|
| 203 |
table { width: 100%; border-collapse: collapse; font-size: 0.875rem; }
|
| 204 |
th {
|
| 205 |
text-align: left; padding: 0.75rem 1rem;
|
|
|
|
| 300 |
}
|
| 301 |
|
| 302 |
/* ── RESPONSIVE ──────────────────────────────── */
|
| 303 |
+
@media (max-width: 1100px) {
|
| 304 |
+
.page { padding: 1.4rem 1rem; }
|
| 305 |
+
.grid-auto { grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); }
|
| 306 |
+
}
|
| 307 |
+
|
| 308 |
@media (max-width: 768px) {
|
| 309 |
.grid-2, .grid-3, .grid-4 { grid-template-columns: 1fr; }
|
| 310 |
.form-row.cols-2, .form-row.cols-3 { grid-template-columns: 1fr; }
|
| 311 |
+
nav { padding: 0 0.8rem; }
|
| 312 |
+
.nav-inner { height: 56px; gap: 0.5rem; }
|
| 313 |
+
.nav-brand { font-size: 1.35rem; letter-spacing: 1px; }
|
| 314 |
.nav-links { display: none; flex-direction: column; position: absolute; top: 60px; left: 0; right: 0;
|
| 315 |
+
background: var(--bg2); padding: 0.8rem; border-bottom: 1px solid var(--border); gap: 0.45rem;
|
| 316 |
+
max-height: calc(100vh - 56px); overflow-y: auto; }
|
| 317 |
+
.nav-links { top: 56px; }
|
| 318 |
.nav-links.open { display: flex; }
|
| 319 |
.hamburger { display: block; margin-left: auto; }
|
| 320 |
+
.nav-link { width: 100%; text-align: left; }
|
| 321 |
+
.pts-badge { margin-left: 0; align-self: flex-start; margin-top: 0.3rem; }
|
| 322 |
+
.alerts { padding: 0 0.9rem; margin-top: 0.75rem; }
|
| 323 |
+
.alert { font-size: 0.84rem; padding: 0.65rem 0.8rem; }
|
| 324 |
+
.page { padding: 0.95rem 0.8rem 1.1rem; }
|
| 325 |
+
.page-header { margin-bottom: 1.25rem; }
|
| 326 |
+
.page-title { font-size: 1.65rem; letter-spacing: 1px; }
|
| 327 |
+
.page-subtitle { font-size: 0.9rem; }
|
| 328 |
+
.card { padding: 1rem; border-radius: 10px; }
|
| 329 |
+
.card-title { font-size: 1.05rem; margin-bottom: 0.75rem; }
|
| 330 |
+
.section-title { font-size: 1.22rem; letter-spacing: 1px; margin-bottom: 0.75rem; gap: 0.55rem; }
|
| 331 |
+
.btn { font-size: 0.84rem; padding: 0.55rem 0.95rem; }
|
| 332 |
+
.btn-sm { font-size: 0.76rem; padding: 0.38rem 0.72rem; }
|
| 333 |
+
input, select, textarea { font-size: 0.88rem; padding: 0.58rem 0.85rem; }
|
| 334 |
+
.match-card { padding: 1rem; }
|
| 335 |
+
.match-vs { gap: 0.55rem; }
|
| 336 |
+
.team-abbr { font-size: 1.5rem; letter-spacing: 1px; }
|
| 337 |
+
.team-name { font-size: 0.72rem; }
|
| 338 |
+
.vs-divider { font-size: 1rem; letter-spacing: 1px; }
|
| 339 |
+
.match-meta { gap: 0.55rem; font-size: 0.75rem; margin-top: 0.62rem; }
|
| 340 |
+
.stat-box { padding: 0.9rem; }
|
| 341 |
+
.stat-value { font-size: 1.55rem; }
|
| 342 |
+
.empty-state { padding: 1.5rem 0.9rem; }
|
| 343 |
+
.empty-state .icon { font-size: 2.2rem; margin-bottom: 0.7rem; }
|
| 344 |
+
.glow { display: none; }
|
| 345 |
+
}
|
| 346 |
+
|
| 347 |
+
@media (max-width: 560px) {
|
| 348 |
+
table { font-size: 0.8rem; }
|
| 349 |
+
th { padding: 0.52rem 0.62rem; font-size: 0.67rem; }
|
| 350 |
+
td { padding: 0.62rem; }
|
| 351 |
+
.btn { width: 100%; }
|
| 352 |
+
.btn.btn-sm { width: auto; }
|
| 353 |
+
.section-title { flex-wrap: wrap; }
|
| 354 |
+
.section-title a { width: auto; }
|
| 355 |
}
|
| 356 |
|
| 357 |
/* ── ACCENT GLOW ─────────────────────────────── */
|
|
|
|
| 367 |
<body>
|
| 368 |
<div class="glow"></div>
|
| 369 |
|
|
|
|
| 370 |
<nav>
|
| 371 |
<div class="nav-inner">
|
| 372 |
<a href="{{ url_for('index') }}" class="nav-brand" title="{{ app_brand }} — pick name / switch user">DIS <span class="brand-ipl">IPL</span> <span class="brand-year">2026</span></a>
|
| 373 |
<button class="hamburger" onclick="toggleNav()">☰</button>
|
| 374 |
<div class="nav-links" id="navLinks">
|
| 375 |
+
{% if current_user %}
|
| 376 |
<a href="{{ url_for('dashboard') }}" class="nav-link {% if request.endpoint == 'dashboard' %}active{% endif %}">🏠 Home</a>
|
| 377 |
<a href="{{ url_for('user_guide') }}" class="nav-link {% if request.endpoint == 'user_guide' %}active{% endif %}">📘 Guide</a>
|
| 378 |
<a href="{{ url_for('matches') }}" class="nav-link {% if request.endpoint == 'matches' %}active{% endif %}">📅 Matches</a>
|
|
|
|
| 380 |
<a href="{{ url_for('leaderboard') }}"class="nav-link {% if request.endpoint == 'leaderboard'%}active{% endif %}">🏆 Board</a>
|
| 381 |
<a href="{{ url_for('analytics') }}" class="nav-link {% if request.endpoint == 'analytics' %}active{% endif %}">📈 Analytics</a>
|
| 382 |
<a href="{{ url_for('history') }}" class="nav-link {% if request.endpoint == 'history' %}active{% endif %}">📊 My Stats</a>
|
| 383 |
+
{% else %}
|
| 384 |
+
<a href="{{ url_for('index') }}" class="nav-link {% if request.endpoint in ['index','identify'] %}active{% endif %}">🏠 Home</a>
|
| 385 |
+
{% endif %}
|
| 386 |
{% if admin_login_configured %}
|
| 387 |
{% if staff_session %}
|
| 388 |
<a href="{{ url_for('admin') }}" class="nav-link {% if request.endpoint and request.endpoint.startswith('admin') %}active{% endif %}">⚙️ Admin</a>
|
|
|
|
| 390 |
<a href="{{ url_for('admin_login', next=url_for('admin')) }}" class="nav-link">🔐 Admin</a>
|
| 391 |
{% endif %}
|
| 392 |
{% endif %}
|
| 393 |
+
{% if current_user %}
|
| 394 |
<span class="pts-badge">{{ '%.0f'|format(current_user.points) }} pts</span>
|
| 395 |
+
{% endif %}
|
| 396 |
</div>
|
| 397 |
</div>
|
| 398 |
</nav>
|
|
|
|
| 399 |
|
| 400 |
<div class="alerts">
|
| 401 |
{% for cat, msg in get_flashed_messages(with_categories=True) %}
|
templates/dashboard.html
CHANGED
|
@@ -4,7 +4,7 @@
|
|
| 4 |
<div class="page">
|
| 5 |
<!-- Header -->
|
| 6 |
<div class="page-header">
|
| 7 |
-
<div style="display:flex; align-items:
|
| 8 |
<div>
|
| 9 |
<div class="page-title">HEY, {{ (current_user.display_name or current_user.username)|upper }} 👋</div>
|
| 10 |
<div class="page-subtitle">
|
|
@@ -21,41 +21,26 @@
|
|
| 21 |
{% endif %}
|
| 22 |
</div>
|
| 23 |
</div>
|
| 24 |
-
<div
|
| 25 |
-
<div class="stat-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
<div class="stat-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
|
|
|
|
|
|
| 37 |
</div>
|
| 38 |
</div>
|
| 39 |
</div>
|
| 40 |
</div>
|
| 41 |
|
| 42 |
-
{% if upcoming_other %}
|
| 43 |
-
<div class="section-title" style="font-size:1.1rem;">🔜 COMING UP NEXT</div>
|
| 44 |
-
<div class="grid grid-auto" style="margin-bottom:2rem;">
|
| 45 |
-
{% for m in upcoming_other %}
|
| 46 |
-
<a href="{{ url_for('matches') }}?date={{ m.match_date }}" class="match-card" style="text-decoration:none; color:inherit;">
|
| 47 |
-
<div style="font-size:0.75rem; color:var(--muted);">{{ m.match_date|format_date }} · {{ m.match_time }}</div>
|
| 48 |
-
<div style="display:flex; align-items:center; justify-content:space-between; margin-top:0.5rem; gap:0.5rem;">
|
| 49 |
-
<span style="font-family:var(--font-display); font-size:1.25rem; color:{{ m.team1_color }};">{{ m.team1_abbr }}</span>
|
| 50 |
-
<span style="color:var(--muted); font-size:0.75rem;">vs</span>
|
| 51 |
-
<span style="font-family:var(--font-display); font-size:1.25rem; color:{{ m.team2_color }};">{{ m.team2_abbr }}</span>
|
| 52 |
-
</div>
|
| 53 |
-
{% if m.venue %}<div style="font-size:0.78rem; color:var(--muted2); margin-top:0.4rem;">📍 {{ m.venue }}</div>{% endif %}
|
| 54 |
-
</a>
|
| 55 |
-
{% endfor %}
|
| 56 |
-
</div>
|
| 57 |
-
{% endif %}
|
| 58 |
-
|
| 59 |
<!-- Today's Matches -->
|
| 60 |
<div class="section-title">
|
| 61 |
🗓️ TODAY'S MATCHES
|
|
@@ -150,6 +135,23 @@
|
|
| 150 |
</div>
|
| 151 |
{% endif %}
|
| 152 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 153 |
<!-- Bottom Grid -->
|
| 154 |
<div class="grid grid-2">
|
| 155 |
<!-- Leaderboard snapshot -->
|
|
|
|
| 4 |
<div class="page">
|
| 5 |
<!-- Header -->
|
| 6 |
<div class="page-header">
|
| 7 |
+
<div style="display:flex; align-items:flex-start; justify-content:space-between; flex-wrap:wrap; gap:1rem;">
|
| 8 |
<div>
|
| 9 |
<div class="page-title">HEY, {{ (current_user.display_name or current_user.username)|upper }} 👋</div>
|
| 10 |
<div class="page-subtitle">
|
|
|
|
| 21 |
{% endif %}
|
| 22 |
</div>
|
| 23 |
</div>
|
| 24 |
+
<div style="display:flex; gap:0.75rem; flex-wrap:wrap;">
|
| 25 |
+
<div class="stat-box" style="min-width:160px;">
|
| 26 |
+
<div class="stat-value">{{ '%.0f'|format(current_user.points) }}</div>
|
| 27 |
+
<div class="stat-label">Your Points</div>
|
| 28 |
+
<div style="font-size:0.78rem; color:var(--muted2); margin-top:0.4rem;">#{{ rank }} on leaderboard</div>
|
| 29 |
+
</div>
|
| 30 |
+
<div class="stat-box" style="min-width:160px;">
|
| 31 |
+
<div class="stat-value" style="font-size:1.75rem;">{{ my_streak }}🔥</div>
|
| 32 |
+
<div class="stat-label">Best win streak</div>
|
| 33 |
+
<div style="font-size:0.78rem; color:var(--muted2); margin-top:0.4rem;">Last 5 IPL results</div>
|
| 34 |
+
<div style="display:flex; justify-content:center; gap:4px; margin-top:0.5rem;">
|
| 35 |
+
{% for c in my_last5 %}
|
| 36 |
+
<span title="{% if c=='green' %}Correct{% elif c=='red' %}Wrong{% else %}No pick / pending{% endif %}" style="width:12px;height:12px;border-radius:50%;display:inline-block;border:1px solid var(--border);{% if c=='green' %}background:var(--green);border-color:var(--green);{% elif c=='red' %}background:var(--red);border-color:var(--red);{% else %}background:var(--bg3);{% endif %}"></span>
|
| 37 |
+
{% endfor %}
|
| 38 |
+
</div>
|
| 39 |
</div>
|
| 40 |
</div>
|
| 41 |
</div>
|
| 42 |
</div>
|
| 43 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 44 |
<!-- Today's Matches -->
|
| 45 |
<div class="section-title">
|
| 46 |
🗓️ TODAY'S MATCHES
|
|
|
|
| 135 |
</div>
|
| 136 |
{% endif %}
|
| 137 |
|
| 138 |
+
{% if upcoming_other %}
|
| 139 |
+
<div class="section-title" style="font-size:1.1rem; margin-top:0.25rem;">🔜 COMING UP NEXT</div>
|
| 140 |
+
<div class="grid grid-auto" style="margin-bottom:2rem;">
|
| 141 |
+
{% for m in upcoming_other %}
|
| 142 |
+
<a href="{{ url_for('matches') }}?date={{ m.match_date }}" class="match-card" style="text-decoration:none; color:inherit;">
|
| 143 |
+
<div style="font-size:0.75rem; color:var(--muted);">{{ m.match_date|format_date }} · {{ m.match_time }}</div>
|
| 144 |
+
<div style="display:flex; align-items:center; justify-content:space-between; margin-top:0.5rem; gap:0.5rem;">
|
| 145 |
+
<span style="font-family:var(--font-display); font-size:1.25rem; color:{{ m.team1_color }};">{{ m.team1_abbr }}</span>
|
| 146 |
+
<span style="color:var(--muted); font-size:0.75rem;">vs</span>
|
| 147 |
+
<span style="font-family:var(--font-display); font-size:1.25rem; color:{{ m.team2_color }};">{{ m.team2_abbr }}</span>
|
| 148 |
+
</div>
|
| 149 |
+
{% if m.venue %}<div style="font-size:0.78rem; color:var(--muted2); margin-top:0.4rem;">📍 {{ m.venue }}</div>{% endif %}
|
| 150 |
+
</a>
|
| 151 |
+
{% endfor %}
|
| 152 |
+
</div>
|
| 153 |
+
{% endif %}
|
| 154 |
+
|
| 155 |
<!-- Bottom Grid -->
|
| 156 |
<div class="grid grid-2">
|
| 157 |
<!-- Leaderboard snapshot -->
|
templates/identify.html
CHANGED
|
@@ -1,27 +1,88 @@
|
|
| 1 |
{% extends 'base.html' %}
|
| 2 |
{% block title %}Home – {{ app_brand }}{% endblock %}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3 |
{% block content %}
|
| 4 |
-
<div class="page
|
| 5 |
-
<div class="card" style="
|
| 6 |
-
<div
|
| 7 |
-
|
| 8 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 9 |
</div>
|
| 10 |
-
<div style="font-size:1rem; color:var(--orange); font-weight:600; margin-top:0.5rem;">DIS IPL Match Predictions</div>
|
| 11 |
-
<div class="page-subtitle" style="margin-top:0.75rem;">🏆 {{ app_tagline }} 🏏</div>
|
| 12 |
-
<div style="font-size:0.88rem; color:var(--muted2); margin-top:0.75rem;">Pick your name to track predictions & points.</div>
|
| 13 |
</div>
|
| 14 |
|
| 15 |
-
<div class="card">
|
| 16 |
<div class="card-title">WHO ARE YOU?</div>
|
| 17 |
{% if current_user %}
|
| 18 |
-
<div
|
| 19 |
-
<div style="font-size:0.
|
| 20 |
-
<div style="font-size:1.
|
| 21 |
-
<a href="{{ url_for('dashboard') }}" class="btn btn-primary" style="margin-top:
|
| 22 |
-
<div style="font-size:0.8rem; color:var(--muted); margin-top:
|
| 23 |
</div>
|
| 24 |
{% endif %}
|
|
|
|
| 25 |
<form method="post" action="{{ url_for('index') }}">
|
| 26 |
<input type="hidden" name="remember" value="1">
|
| 27 |
<div class="form-group">
|
|
@@ -36,11 +97,19 @@
|
|
| 36 |
{% endfor %}
|
| 37 |
</select>
|
| 38 |
</div>
|
| 39 |
-
<button type="submit" class="btn btn-primary" style="width:100%; justify-content:center; padding:0.
|
| 40 |
</form>
|
| 41 |
-
|
|
|
|
| 42 |
New teammate? Ask your admin to add you to the team roster.
|
| 43 |
</p>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 44 |
</div>
|
| 45 |
</div>
|
| 46 |
{% endblock %}
|
|
|
|
| 1 |
{% extends 'base.html' %}
|
| 2 |
{% block title %}Home – {{ app_brand }}{% endblock %}
|
| 3 |
+
{% block head %}
|
| 4 |
+
<style>
|
| 5 |
+
.home-wrap { max-width: 620px; }
|
| 6 |
+
.home-hero {
|
| 7 |
+
position: relative;
|
| 8 |
+
overflow: hidden;
|
| 9 |
+
border-color: rgba(249,115,22,0.28);
|
| 10 |
+
background:
|
| 11 |
+
radial-gradient(circle at 10% 10%, rgba(249,115,22,0.18), transparent 48%),
|
| 12 |
+
radial-gradient(circle at 90% 20%, rgba(59,130,246,0.14), transparent 44%),
|
| 13 |
+
linear-gradient(165deg, var(--card), #0f182b);
|
| 14 |
+
}
|
| 15 |
+
.home-hero:after {
|
| 16 |
+
content: "";
|
| 17 |
+
position: absolute;
|
| 18 |
+
inset: 0;
|
| 19 |
+
pointer-events: none;
|
| 20 |
+
background: linear-gradient(120deg, transparent 0%, rgba(255,255,255,0.03) 45%, transparent 100%);
|
| 21 |
+
}
|
| 22 |
+
.home-hero-inner { position: relative; z-index: 1; text-align: center; }
|
| 23 |
+
.home-title { font-size: 2rem; letter-spacing: 3px; }
|
| 24 |
+
.home-sub {
|
| 25 |
+
font-size: 0.95rem;
|
| 26 |
+
color: var(--muted2);
|
| 27 |
+
margin-top: 0.75rem;
|
| 28 |
+
line-height: 1.45;
|
| 29 |
+
}
|
| 30 |
+
.home-chip-row {
|
| 31 |
+
display: flex;
|
| 32 |
+
justify-content: center;
|
| 33 |
+
gap: 0.5rem;
|
| 34 |
+
flex-wrap: wrap;
|
| 35 |
+
margin-top: 1rem;
|
| 36 |
+
}
|
| 37 |
+
.home-chip {
|
| 38 |
+
font-size: 0.72rem;
|
| 39 |
+
letter-spacing: 0.05em;
|
| 40 |
+
padding: 0.3rem 0.55rem;
|
| 41 |
+
border-radius: 999px;
|
| 42 |
+
border: 1px solid var(--border);
|
| 43 |
+
background: rgba(10,14,26,0.45);
|
| 44 |
+
color: var(--muted2);
|
| 45 |
+
}
|
| 46 |
+
.identity-panel { border-color: rgba(148,163,184,0.28); }
|
| 47 |
+
.current-user-panel {
|
| 48 |
+
text-align: center;
|
| 49 |
+
padding: 1rem;
|
| 50 |
+
border: 1px solid rgba(34,197,94,0.28);
|
| 51 |
+
border-radius: var(--radius-sm);
|
| 52 |
+
background: rgba(34,197,94,0.06);
|
| 53 |
+
margin-bottom: 1rem;
|
| 54 |
+
}
|
| 55 |
+
</style>
|
| 56 |
+
{% endblock %}
|
| 57 |
{% block content %}
|
| 58 |
+
<div class="page home-wrap">
|
| 59 |
+
<div class="card home-hero" style="margin-bottom:1.25rem;">
|
| 60 |
+
<div class="home-hero-inner">
|
| 61 |
+
<div style="font-size:2.4rem; margin-bottom:0.35rem;">🏏</div>
|
| 62 |
+
<div class="page-title home-title">
|
| 63 |
+
DIS <span style="color:var(--white);">IPL</span> <span style="color:var(--gold);">2026</span>
|
| 64 |
+
</div>
|
| 65 |
+
<div style="font-size:1.02rem; color:var(--orange); font-weight:700; margin-top:0.35rem;">DIS IPL Match Predictions</div>
|
| 66 |
+
<div class="home-sub">🏆 {{ app_tagline }} 🏏</div>
|
| 67 |
+
<div class="home-chip-row">
|
| 68 |
+
<span class="home-chip">Team pool</span>
|
| 69 |
+
<span class="home-chip">Match-day picks</span>
|
| 70 |
+
<span class="home-chip">Live leaderboard</span>
|
| 71 |
+
</div>
|
| 72 |
</div>
|
|
|
|
|
|
|
|
|
|
| 73 |
</div>
|
| 74 |
|
| 75 |
+
<div class="card identity-panel">
|
| 76 |
<div class="card-title">WHO ARE YOU?</div>
|
| 77 |
{% if current_user %}
|
| 78 |
+
<div class="current-user-panel">
|
| 79 |
+
<div style="font-size:0.82rem; color:var(--muted2);">You’re signed in as</div>
|
| 80 |
+
<div style="font-size:1.28rem; font-weight:700; color:var(--orange); margin-top:0.25rem;">{{ current_user.display_name or current_user.username }}</div>
|
| 81 |
+
<a href="{{ url_for('dashboard') }}" class="btn btn-primary" style="margin-top:0.9rem; width:100%; max-width:310px; justify-content:center;">Open app — today’s matches</a>
|
| 82 |
+
<div style="font-size:0.8rem; color:var(--muted); margin-top:0.8rem;">Switch user? Select another teammate below.</div>
|
| 83 |
</div>
|
| 84 |
{% endif %}
|
| 85 |
+
|
| 86 |
<form method="post" action="{{ url_for('index') }}">
|
| 87 |
<input type="hidden" name="remember" value="1">
|
| 88 |
<div class="form-group">
|
|
|
|
| 97 |
{% endfor %}
|
| 98 |
</select>
|
| 99 |
</div>
|
| 100 |
+
<button type="submit" class="btn btn-primary" style="width:100%; justify-content:center; padding:0.9rem;">Let’s go →</button>
|
| 101 |
</form>
|
| 102 |
+
|
| 103 |
+
<p style="margin-top:1rem; font-size:0.8rem; color:var(--muted); text-align:center;">
|
| 104 |
New teammate? Ask your admin to add you to the team roster.
|
| 105 |
</p>
|
| 106 |
+
<p style="margin-top:0.5rem; font-size:0.8rem; text-align:center;">
|
| 107 |
+
{% if admin_login_configured %}
|
| 108 |
+
<a href="{{ url_for('admin_login', next=url_for('admin')) }}" style="color:var(--orange);">🔐 Admin login</a>
|
| 109 |
+
{% else %}
|
| 110 |
+
<span style="color:var(--muted);">Admin login is not enabled yet.</span>
|
| 111 |
+
{% endif %}
|
| 112 |
+
</p>
|
| 113 |
</div>
|
| 114 |
</div>
|
| 115 |
{% endblock %}
|
templates/leaderboard.html
CHANGED
|
@@ -7,6 +7,9 @@
|
|
| 7 |
overflow: auto;
|
| 8 |
-webkit-overflow-scrolling: touch;
|
| 9 |
}
|
|
|
|
|
|
|
|
|
|
| 10 |
.leaderboard-table-wrap thead th {
|
| 11 |
position: sticky;
|
| 12 |
top: 0;
|
|
@@ -14,6 +17,71 @@
|
|
| 14 |
background: var(--card);
|
| 15 |
box-shadow: 0 1px 0 var(--border);
|
| 16 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 17 |
</style>
|
| 18 |
{% endblock %}
|
| 19 |
{% block content %}
|
|
@@ -25,45 +93,45 @@
|
|
| 25 |
|
| 26 |
<!-- Podium (top 3) -->
|
| 27 |
{% if players|length >= 3 %}
|
| 28 |
-
<div
|
| 29 |
<!-- 2nd -->
|
| 30 |
-
<div style="text-align:center; flex:0 0 180px;">
|
| 31 |
<div style="font-size:2rem;">🥈</div>
|
| 32 |
<div style="background:var(--card); border:1px solid var(--border); border-radius:12px 12px 0 0; padding:1rem 0.75rem; border-bottom:none;">
|
| 33 |
-
<div style="font-family:var(--font-display); font-size:1.1rem; color:var(--muted2);">{{ players[1].display_name or players[1].username }}</div>
|
| 34 |
-
<div style="font-family:var(--font-mono); font-size:1.5rem; color:var(--muted2); font-weight:700;">{{ '%.0f'|format(players[1].points) }}</div>
|
| 35 |
<div style="font-size:0.75rem; color:var(--muted);">pts</div>
|
| 36 |
{% if players[1].settled_count > 0 %}
|
| 37 |
<div style="font-size:0.75rem; color:var(--muted); margin-top:0.5rem;">{{ players[1].correct_winners }}/{{ players[1].settled_count }} wins</div>
|
| 38 |
{% endif %}
|
| 39 |
</div>
|
| 40 |
-
<div style="background:var(--bg3); border:1px solid var(--border); height:80px; border-radius:0 0 8px 8px;"></div>
|
| 41 |
</div>
|
| 42 |
<!-- 1st -->
|
| 43 |
-
<div style="text-align:center; flex:0 0 200px;">
|
| 44 |
<div style="font-size:2.5rem;">🥇</div>
|
| 45 |
<div style="background:linear-gradient(135deg,var(--card),rgba(251,191,36,0.08)); border:1px solid rgba(251,191,36,0.3); border-radius:12px 12px 0 0; padding:1.25rem 0.75rem; border-bottom:none;">
|
| 46 |
-
<div style="font-family:var(--font-display); font-size:1.3rem; color:var(--gold);">{{ players[0].display_name or players[0].username }}</div>
|
| 47 |
-
<div style="font-family:var(--font-mono); font-size:2rem; color:var(--gold); font-weight:700;">{{ '%.0f'|format(players[0].points) }}</div>
|
| 48 |
<div style="font-size:0.75rem; color:var(--muted);">pts</div>
|
| 49 |
{% if players[0].settled_count > 0 %}
|
| 50 |
<div style="font-size:0.75rem; color:var(--muted); margin-top:0.5rem;">{{ players[0].correct_winners }}/{{ players[0].settled_count }} wins</div>
|
| 51 |
{% endif %}
|
| 52 |
</div>
|
| 53 |
-
<div style="background:linear-gradient(to bottom, rgba(251,191,36,0.1), var(--bg3)); border:1px solid var(--border); height:110px; border-radius:0 0 8px 8px;"></div>
|
| 54 |
</div>
|
| 55 |
<!-- 3rd -->
|
| 56 |
-
<div style="text-align:center; flex:0 0 180px;">
|
| 57 |
<div style="font-size:2rem;">🥉</div>
|
| 58 |
<div style="background:var(--card); border:1px solid var(--border); border-radius:12px 12px 0 0; padding:1rem 0.75rem; border-bottom:none;">
|
| 59 |
-
<div style="font-family:var(--font-display); font-size:1.1rem; color:#cd7f32;">{{ players[2].display_name or players[2].username }}</div>
|
| 60 |
-
<div style="font-family:var(--font-mono); font-size:1.5rem; color:#cd7f32; font-weight:700;">{{ '%.0f'|format(players[2].points) }}</div>
|
| 61 |
<div style="font-size:0.75rem; color:var(--muted);">pts</div>
|
| 62 |
{% if players[2].settled_count > 0 %}
|
| 63 |
<div style="font-size:0.75rem; color:var(--muted); margin-top:0.5rem;">{{ players[2].correct_winners }}/{{ players[2].settled_count }} wins</div>
|
| 64 |
{% endif %}
|
| 65 |
</div>
|
| 66 |
-
<div style="background:var(--bg3); border:1px solid var(--border); height:55px; border-radius:0 0 8px 8px;"></div>
|
| 67 |
</div>
|
| 68 |
</div>
|
| 69 |
{% endif %}
|
|
|
|
| 7 |
overflow: auto;
|
| 8 |
-webkit-overflow-scrolling: touch;
|
| 9 |
}
|
| 10 |
+
.leaderboard-table-wrap table {
|
| 11 |
+
min-width: 880px;
|
| 12 |
+
}
|
| 13 |
.leaderboard-table-wrap thead th {
|
| 14 |
position: sticky;
|
| 15 |
top: 0;
|
|
|
|
| 17 |
background: var(--card);
|
| 18 |
box-shadow: 0 1px 0 var(--border);
|
| 19 |
}
|
| 20 |
+
.podium-wrap {
|
| 21 |
+
display: flex;
|
| 22 |
+
align-items: flex-end;
|
| 23 |
+
justify-content: center;
|
| 24 |
+
gap: 1rem;
|
| 25 |
+
margin-bottom: 2.5rem;
|
| 26 |
+
flex-wrap: wrap;
|
| 27 |
+
}
|
| 28 |
+
@media (max-width: 760px) {
|
| 29 |
+
.podium-wrap {
|
| 30 |
+
display: flex;
|
| 31 |
+
align-items: flex-end;
|
| 32 |
+
justify-content: center;
|
| 33 |
+
gap: 0.45rem;
|
| 34 |
+
margin-bottom: 1.2rem;
|
| 35 |
+
flex-wrap: nowrap;
|
| 36 |
+
}
|
| 37 |
+
.podium-wrap > div { min-width: 0; flex: 1 1 0 !important; }
|
| 38 |
+
.podium-wrap .podium-rank-1 { order: 2; }
|
| 39 |
+
.podium-wrap .podium-rank-2 { order: 1; }
|
| 40 |
+
.podium-wrap .podium-rank-3 { order: 3; }
|
| 41 |
+
.podium-wrap .podium-name { font-size: 0.95rem !important; }
|
| 42 |
+
.podium-wrap .podium-pts { font-size: 1.15rem !important; }
|
| 43 |
+
.podium-wrap .podium-col { height: 46px !important; }
|
| 44 |
+
.leaderboard-table-wrap table {
|
| 45 |
+
min-width: 760px;
|
| 46 |
+
font-size: 0.84rem;
|
| 47 |
+
}
|
| 48 |
+
.leaderboard-table-wrap thead th:nth-child(1),
|
| 49 |
+
.leaderboard-table-wrap tbody td:nth-child(1) {
|
| 50 |
+
position: sticky;
|
| 51 |
+
left: 0;
|
| 52 |
+
z-index: 3;
|
| 53 |
+
background: var(--card);
|
| 54 |
+
box-shadow: 1px 0 0 var(--border);
|
| 55 |
+
}
|
| 56 |
+
.leaderboard-table-wrap thead th:nth-child(2),
|
| 57 |
+
.leaderboard-table-wrap tbody td:nth-child(2) {
|
| 58 |
+
position: sticky;
|
| 59 |
+
left: 52px;
|
| 60 |
+
z-index: 3;
|
| 61 |
+
background: var(--card);
|
| 62 |
+
box-shadow: 1px 0 0 var(--border);
|
| 63 |
+
min-width: 140px;
|
| 64 |
+
}
|
| 65 |
+
.leaderboard-table-wrap tbody td { padding-top: 0.5rem; padding-bottom: 0.5rem; }
|
| 66 |
+
}
|
| 67 |
+
@media (max-width: 560px) {
|
| 68 |
+
.podium-wrap { gap: 0.35rem; margin-bottom: 1rem; }
|
| 69 |
+
.podium-wrap .podium-name { font-size: 0.84rem !important; }
|
| 70 |
+
.podium-wrap .podium-pts { font-size: 1rem !important; }
|
| 71 |
+
.podium-wrap .podium-col { height: 34px !important; }
|
| 72 |
+
.leaderboard-table-wrap thead th:nth-child(4),
|
| 73 |
+
.leaderboard-table-wrap tbody td:nth-child(4),
|
| 74 |
+
.leaderboard-table-wrap thead th:nth-child(6),
|
| 75 |
+
.leaderboard-table-wrap tbody td:nth-child(6),
|
| 76 |
+
.leaderboard-table-wrap thead th:nth-child(8),
|
| 77 |
+
.leaderboard-table-wrap tbody td:nth-child(8) {
|
| 78 |
+
display: none;
|
| 79 |
+
}
|
| 80 |
+
.leaderboard-table-wrap table {
|
| 81 |
+
min-width: 640px;
|
| 82 |
+
font-size: 0.8rem;
|
| 83 |
+
}
|
| 84 |
+
}
|
| 85 |
</style>
|
| 86 |
{% endblock %}
|
| 87 |
{% block content %}
|
|
|
|
| 93 |
|
| 94 |
<!-- Podium (top 3) -->
|
| 95 |
{% if players|length >= 3 %}
|
| 96 |
+
<div class="podium-wrap">
|
| 97 |
<!-- 2nd -->
|
| 98 |
+
<div class="podium-rank-2" style="text-align:center; flex:0 0 180px;">
|
| 99 |
<div style="font-size:2rem;">🥈</div>
|
| 100 |
<div style="background:var(--card); border:1px solid var(--border); border-radius:12px 12px 0 0; padding:1rem 0.75rem; border-bottom:none;">
|
| 101 |
+
<div class="podium-name" style="font-family:var(--font-display); font-size:1.1rem; color:var(--muted2);">{{ players[1].display_name or players[1].username }}</div>
|
| 102 |
+
<div class="podium-pts" style="font-family:var(--font-mono); font-size:1.5rem; color:var(--muted2); font-weight:700;">{{ '%.0f'|format(players[1].points) }}</div>
|
| 103 |
<div style="font-size:0.75rem; color:var(--muted);">pts</div>
|
| 104 |
{% if players[1].settled_count > 0 %}
|
| 105 |
<div style="font-size:0.75rem; color:var(--muted); margin-top:0.5rem;">{{ players[1].correct_winners }}/{{ players[1].settled_count }} wins</div>
|
| 106 |
{% endif %}
|
| 107 |
</div>
|
| 108 |
+
<div class="podium-col" style="background:var(--bg3); border:1px solid var(--border); height:80px; border-radius:0 0 8px 8px;"></div>
|
| 109 |
</div>
|
| 110 |
<!-- 1st -->
|
| 111 |
+
<div class="podium-rank-1" style="text-align:center; flex:0 0 200px;">
|
| 112 |
<div style="font-size:2.5rem;">🥇</div>
|
| 113 |
<div style="background:linear-gradient(135deg,var(--card),rgba(251,191,36,0.08)); border:1px solid rgba(251,191,36,0.3); border-radius:12px 12px 0 0; padding:1.25rem 0.75rem; border-bottom:none;">
|
| 114 |
+
<div class="podium-name" style="font-family:var(--font-display); font-size:1.3rem; color:var(--gold);">{{ players[0].display_name or players[0].username }}</div>
|
| 115 |
+
<div class="podium-pts" style="font-family:var(--font-mono); font-size:2rem; color:var(--gold); font-weight:700;">{{ '%.0f'|format(players[0].points) }}</div>
|
| 116 |
<div style="font-size:0.75rem; color:var(--muted);">pts</div>
|
| 117 |
{% if players[0].settled_count > 0 %}
|
| 118 |
<div style="font-size:0.75rem; color:var(--muted); margin-top:0.5rem;">{{ players[0].correct_winners }}/{{ players[0].settled_count }} wins</div>
|
| 119 |
{% endif %}
|
| 120 |
</div>
|
| 121 |
+
<div class="podium-col" style="background:linear-gradient(to bottom, rgba(251,191,36,0.1), var(--bg3)); border:1px solid var(--border); height:110px; border-radius:0 0 8px 8px;"></div>
|
| 122 |
</div>
|
| 123 |
<!-- 3rd -->
|
| 124 |
+
<div class="podium-rank-3" style="text-align:center; flex:0 0 180px;">
|
| 125 |
<div style="font-size:2rem;">🥉</div>
|
| 126 |
<div style="background:var(--card); border:1px solid var(--border); border-radius:12px 12px 0 0; padding:1rem 0.75rem; border-bottom:none;">
|
| 127 |
+
<div class="podium-name" style="font-family:var(--font-display); font-size:1.1rem; color:#cd7f32;">{{ players[2].display_name or players[2].username }}</div>
|
| 128 |
+
<div class="podium-pts" style="font-family:var(--font-mono); font-size:1.5rem; color:#cd7f32; font-weight:700;">{{ '%.0f'|format(players[2].points) }}</div>
|
| 129 |
<div style="font-size:0.75rem; color:var(--muted);">pts</div>
|
| 130 |
{% if players[2].settled_count > 0 %}
|
| 131 |
<div style="font-size:0.75rem; color:var(--muted); margin-top:0.5rem;">{{ players[2].correct_winners }}/{{ players[2].settled_count }} wins</div>
|
| 132 |
{% endif %}
|
| 133 |
</div>
|
| 134 |
+
<div class="podium-col" style="background:var(--bg3); border:1px solid var(--border); height:55px; border-radius:0 0 8px 8px;"></div>
|
| 135 |
</div>
|
| 136 |
</div>
|
| 137 |
{% endif %}
|
users.json
CHANGED
|
@@ -6,7 +6,6 @@
|
|
| 6 |
{"key": "jay", "display_name": "Jay"},
|
| 7 |
{"key": "kishore", "display_name": "Kishore"},
|
| 8 |
{"key": "megha", "display_name": "Megha"},
|
| 9 |
-
{"key": "naveein", "display_name": "Naveein"},
|
| 10 |
{"key": "neha", "display_name": "Neha"},
|
| 11 |
{"key": "praveen", "display_name": "Praveen"},
|
| 12 |
{"key": "rakesh", "display_name": "Rakesh"},
|
|
|
|
| 6 |
{"key": "jay", "display_name": "Jay"},
|
| 7 |
{"key": "kishore", "display_name": "Kishore"},
|
| 8 |
{"key": "megha", "display_name": "Megha"},
|
|
|
|
| 9 |
{"key": "neha", "display_name": "Neha"},
|
| 10 |
{"key": "praveen", "display_name": "Praveen"},
|
| 11 |
{"key": "rakesh", "display_name": "Rakesh"},
|