Jay-Rajput commited on
Commit
47370ee
·
1 Parent(s): 857d022

Admin panel imprvmnts

Browse files
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': '#3A225D', 'SRH': '#FF822A', 'DC': '#0078BC',
267
- 'RR': '#EA1A85', 'PBKS': '#AA4545', 'LSG': '#A4C639', 'GT': '#1C1C1C',
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) == date.today()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
641
 
642
 
643
  def is_prediction_locked(match) -> bool:
644
  if match['status'] not in ('upcoming',):
645
  return True
646
  try:
647
- match_dt_str = f"{match['match_date']} {match['match_time']}"
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 datetime.now() >= lock_dt
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
- match_dt_str = f"{row['match_date']} {row['match_time']}"
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 datetime.now() >= lock_dt:
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 = date.today().isoformat()
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': date.today().isoformat(),
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 { overflow-x: auto; }
 
 
 
 
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: 1rem; border-bottom: 1px solid var(--border); gap: 0.5rem; }
 
 
302
  .nav-links.open { display: flex; }
303
  .hamburger { display: block; margin-left: auto; }
304
- .page { padding: 1rem; }
305
- .page-title { font-size: 1.8rem; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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:center; 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,41 +21,26 @@
21
  {% endif %}
22
  </div>
23
  </div>
24
- <div class="stat-box" style="min-width:160px;">
25
- <div class="stat-value">{{ '%.0f'|format(current_user.points) }}</div>
26
- <div class="stat-label">Your Points</div>
27
- <div style="font-size:0.78rem; color:var(--muted2); margin-top:0.4rem;">#{{ rank }} on leaderboard</div>
28
- </div>
29
- <div class="stat-box" style="min-width:140px;">
30
- <div class="stat-value" style="font-size:1.75rem;">{{ my_streak }}🔥</div>
31
- <div class="stat-label">Best win streak</div>
32
- <div style="font-size:0.78rem; color:var(--muted2); margin-top:0.4rem;">Last 5 IPL results</div>
33
- <div style="display:flex; justify-content:center; gap:4px; margin-top:0.5rem;">
34
- {% for c in my_last5 %}
35
- <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>
36
- {% endfor %}
 
 
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" style="max-width:520px;">
5
- <div class="card" style="text-align:center; margin-bottom:1.5rem; border-color:rgba(249,115,22,0.25);">
6
- <div style="font-size:2.5rem; margin-bottom:0.5rem;">🏏</div>
7
- <div class="page-title" style="font-size:1.75rem; letter-spacing:3px;">
8
- DIS <span style="color:var(--white);">IPL</span> <span style="color:var(--gold);">2026</span>
 
 
 
 
 
 
 
 
 
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 &amp; points.</div>
13
  </div>
14
 
15
- <div class="card">
16
  <div class="card-title">WHO ARE YOU?</div>
17
  {% if current_user %}
18
- <div style="text-align:center; padding-bottom:1.25rem; margin-bottom:1.25rem; border-bottom:1px solid var(--border);">
19
- <div style="font-size:0.85rem; color:var(--muted2);">You’re signed in as</div>
20
- <div style="font-size:1.2rem; font-weight:700; color:var(--orange); margin-top:0.35rem;">{{ current_user.display_name or current_user.username }}</div>
21
- <a href="{{ url_for('dashboard') }}" class="btn btn-primary" style="margin-top:1rem; width:100%; max-width:280px; justify-content:center;">Open app — today’s matches</a>
22
- <div style="font-size:0.8rem; color:var(--muted); margin-top:1rem;">Someone else on this device? Pick their name below.</div>
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.85rem;">Let’s go →</button>
40
  </form>
41
- <p style="margin-top:1.25rem; font-size:0.8rem; color:var(--muted);">
 
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 style="display:flex; align-items:flex-end; justify-content:center; gap:1rem; margin-bottom:2.5rem; flex-wrap:wrap;">
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"},