Spaces:
Sleeping
Sleeping
| {% extends 'base.html' %} | |
| {% block title %}Predict – {{ match.team1 }} vs {{ match.team2 }} – {{ app_brand }}{% endblock %} | |
| {% block head %} | |
| <style> | |
| .motm-lead { font-size: 0.88rem; color: var(--muted2); margin: 0.35rem 0 0.65rem; line-height: 1.45; } | |
| #predicted_motm:disabled { | |
| opacity: 0.65; cursor: not-allowed; | |
| } | |
| </style> | |
| {% endblock %} | |
| {% block content %} | |
| <div class="page" style="max-width:680px;"> | |
| <div style="margin-bottom:1rem;"> | |
| <a href="{{ url_for('dashboard') }}" style="color:var(--muted2); text-decoration:none; font-size:0.875rem;">← Back to Dashboard</a> | |
| </div> | |
| <!-- Match Banner --> | |
| <div class="card" style="margin-bottom:1.5rem; background:linear-gradient(135deg, var(--card) 0%, rgba(249,115,22,0.05) 100%); border-color:rgba(249,115,22,0.2);"> | |
| <div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:1rem;"> | |
| <span class="badge badge-{{ match.status }}"> | |
| {% if match.status == 'live' %}<span class="status-dot dot-live"></span>{% endif %} | |
| {{ match.status|upper }} | |
| </span> | |
| {% if match.match_number %}<span style="font-size:0.8rem; color:var(--muted);">Match #{{ match.match_number }}</span>{% endif %} | |
| </div> | |
| <div class="match-vs"> | |
| <div class="team-block"> | |
| <div class="team-abbr" style="font-size:3rem; color:{{ match.team1_color }};">{{ match.team1_abbr }}</div> | |
| <div class="team-name" style="font-size:0.9rem;">{{ match.team1 }}</div> | |
| </div> | |
| <div class="vs-divider" style="font-size:1.8rem;">VS</div> | |
| <div class="team-block"> | |
| <div class="team-abbr" style="font-size:3rem; color:{{ match.team2_color }};">{{ match.team2_abbr }}</div> | |
| <div class="team-name" style="font-size:0.9rem;">{{ match.team2 }}</div> | |
| </div> | |
| </div> | |
| <div class="match-meta" style="margin-top:1rem; justify-content:center;"> | |
| <span>🗓️ {{ match.match_time_display }}</span> | |
| {% if match.venue %}<span>📍 {{ match.venue }}{% if match.city %}, {{ match.city }}{% endif %}</span>{% endif %} | |
| </div> | |
| <div style="text-align:center; margin-top:0.75rem; font-size:0.82rem; color:var(--muted);"> | |
| 📅 Open on <strong>match day only</strong> · locks at scheduled start ({{ match.match_time_display }}) | |
| </div> | |
| </div> | |
| {% if match.status == 'completed' %} | |
| <!-- Result Display --> | |
| <div class="card" style="border-color:rgba(34,197,94,0.3); background:rgba(34,197,94,0.05);"> | |
| <div class="card-title" style="color:var(--green);">🏆 MATCH RESULT</div> | |
| <div style="font-size:1.5rem; font-weight:700;">{{ match.winner }}</div> | |
| {% if match.man_of_match %}<div style="color:var(--muted2); margin-top:0.5rem;">⭐ Man of the Match: <strong>{{ match.man_of_match }}</strong></div>{% endif %} | |
| {% if match.result_notes %}<div style="color:var(--muted2); font-size:0.85rem; margin-top:0.5rem;">{{ match.result_notes }}</div>{% endif %} | |
| {% if existing %} | |
| <hr> | |
| <div style="display:flex; gap:1.5rem; flex-wrap:wrap;"> | |
| <div> | |
| <div style="font-size:0.8rem; color:var(--muted2);">YOUR PICK</div> | |
| <div style="font-weight:700;">{{ existing.predicted_winner }}</div> | |
| {% if existing.predicted_motm %}<div style="font-size:0.85rem; color:var(--muted2);">⭐ {{ existing.predicted_motm }}</div>{% endif %} | |
| </div> | |
| <div> | |
| <div style="font-size:0.8rem; color:var(--muted2);">BID</div> | |
| <div style="font-weight:700; font-family:var(--font-mono);">{{ '%.0f'|format(existing.bid_amount) }} pts</div> | |
| </div> | |
| {% if existing.is_settled %} | |
| <div> | |
| <div style="font-size:0.8rem; color:var(--muted2);">RESULT</div> | |
| <div style="font-weight:700;" class="{{ existing.points_earned|delta_class }}">{{ existing.points_earned|delta_sign }} pts</div> | |
| <div style="font-size:0.78rem; color:var(--muted);">Winner: {% if existing.winner_correct %}✅{% else %}❌{% endif %} {% if existing.motm_correct is not none %}· MOTM: {% if existing.motm_correct %}✅{% else %}❌{% endif %}{% endif %}</div> | |
| </div> | |
| {% endif %} | |
| </div> | |
| {% endif %} | |
| </div> | |
| {% elif match.can_predict %} | |
| <!-- Prediction Form --> | |
| <div class="card"> | |
| <div class="card-title">{% if existing %}✏️ EDIT PREDICTION{% else %}🎯 MAKE YOUR PREDICTION{% endif %}</div> | |
| <!-- Current points display --> | |
| <div style="display:flex; justify-content:space-between; align-items:center; padding:0.75rem 1rem; background:var(--bg3); border-radius:8px; margin-bottom:1.5rem;"> | |
| <div style="font-size:0.875rem; color:var(--muted2);">Your current balance</div> | |
| <div style="font-family:var(--font-mono); font-size:1.25rem; font-weight:700;"> | |
| <span style="background:linear-gradient(135deg,var(--orange),var(--gold)); -webkit-background-clip:text; -webkit-text-fill-color:transparent;"> | |
| {{ '%.0f'|format(current_user.points) }} | |
| </span> | |
| <span style="color:var(--muted); font-size:0.8rem;"> pts</span> | |
| </div> | |
| </div> | |
| <form method="post" id="predictForm"> | |
| <!-- Winner selection --> | |
| <div class="form-group"> | |
| <label>🏆 Who will WIN? <span style="color:var(--red);">*</span></label> | |
| <div style="display:grid; grid-template-columns:1fr 1fr; gap:0.75rem; margin-top:0.5rem;"> | |
| {% for team in [match.team1, match.team2] %} | |
| {% set abbr = team_abbr.get(team, team[:3].upper()) %} | |
| {% set color = team_colors.get(abbr, '#555') %} | |
| <label class="team-pick-label" style="cursor:pointer; margin:0;"> | |
| <input type="radio" name="predicted_winner" value="{{ team }}" required | |
| {% if existing and existing.predicted_winner == team %}checked{% endif %} | |
| style="display:none;" onchange="onWinnerChange(this)"> | |
| <div class="team-pick-card" id="pick-{{ abbr }}" style="border:2px solid var(--border); border-radius:10px; padding:1rem; text-align:center; transition:all 0.2s; cursor:pointer; {% if existing and existing.predicted_winner == team %}border-color:{{ color }}; background:{{ color }}22;{% endif %}"> | |
| <div style="font-family:var(--font-display); font-size:2rem; color:{{ color }};">{{ abbr }}</div> | |
| <div style="font-size:0.78rem; color:var(--muted2); margin-top:0.2rem;">{{ team }}</div> | |
| </div> | |
| </label> | |
| {% endfor %} | |
| </div> | |
| </div> | |
| <!-- MOTM: required; options = squad of predicted winner only --> | |
| <div class="form-group"> | |
| <label for="predicted_motm">⭐ Man of the Match <span style="color:var(--red);">*</span></label> | |
| <p class="motm-lead"> | |
| Choose from the <strong>squad of the team you picked to win</strong>. | |
| <strong style="color:var(--green);">+{{ points_config.correct_motm }}</strong> if it matches the official call · | |
| <strong style="color:var(--red);">−{{ points_config.wrong_motm|abs }}</strong> if it doesn’t. | |
| </p> | |
| <select name="predicted_motm" id="predicted_motm" required disabled aria-label="Man of the Match"> | |
| <option value="">— Pick winning team first —</option> | |
| </select> | |
| <p id="motmEmptyHint" class="form-hint" style="display:none; margin-top:0.5rem;"> | |
| No players are listed for that team’s squad — you can’t submit until the roster is updated. | |
| </p> | |
| </div> | |
| <!-- Bid --> | |
| <div class="form-group"> | |
| <label>💰 Bid Amount <span style="color:var(--red);">*</span></label> | |
| <div style="position:relative;"> | |
| <input type="number" name="bid_amount" id="bidInput" | |
| min="{{ points_config.min_bid }}" | |
| max="{{ [points_config.max_bid, current_user.points|int]|min }}" | |
| step="5" | |
| value="{{ existing.bid_amount|int if existing else points_config.min_bid }}" | |
| required oninput="updateBidDisplay(this.value)"> | |
| <span style="position:absolute; right:1rem; top:50%; transform:translateY(-50%); color:var(--muted); font-size:0.85rem;">pts</span> | |
| </div> | |
| <div class="form-hint"> | |
| Min: <strong>{{ points_config.min_bid }}</strong> · Max: <strong>{{ [points_config.max_bid, current_user.points|int]|min }}</strong> | |
| · Win: <strong class="text-green" id="winAmount">+{{ (existing.bid_amount|int if existing else points_config.min_bid) }}</strong> | |
| · Lose: <strong class="text-red" id="loseAmount">−{{ (existing.bid_amount|int if existing else points_config.min_bid) }}</strong> | |
| </div> | |
| <!-- Quick bid buttons --> | |
| <div style="display:flex; gap:0.5rem; margin-top:0.75rem; flex-wrap:wrap;"> | |
| {% set pts = current_user.points|int %} | |
| {% set max_b = [points_config.max_bid, pts]|min %} | |
| {% for pct in [10, 25, 50, 75, 100] %} | |
| {% set amt = (max_b * pct / 100)|int %} | |
| {% if amt >= points_config.min_bid %} | |
| <button type="button" class="btn btn-ghost btn-sm" onclick="setBid({{ amt }})">{{ pct }}% ({{ amt }})</button> | |
| {% endif %} | |
| {% endfor %} | |
| </div> | |
| </div> | |
| <!-- Potential outcome preview --> | |
| <div id="outcomePreview" style="padding:1rem; background:var(--bg3); border-radius:8px; margin-bottom:1.5rem; font-size:0.85rem;"> | |
| <div style="color:var(--muted2); margin-bottom:0.5rem; font-size:0.78rem; text-transform:uppercase; letter-spacing:0.5px;">Potential Outcomes</div> | |
| <div style="display:grid; grid-template-columns:1fr 1fr; gap:0.5rem;"> | |
| <div>✅ Win + correct MOTM: <strong class="text-green" id="bestCase">—</strong></div> | |
| <div>✅ Win + wrong MOTM: <strong class="text-green" id="winWrongMotm">—</strong></div> | |
| <div>❌ Lose + correct MOTM: <strong id="loseCorrectMotm">—</strong></div> | |
| <div>❌ Lose + wrong MOTM: <strong class="text-red" id="loseWrongMotm">—</strong></div> | |
| </div> | |
| </div> | |
| <button type="submit" class="btn btn-primary" style="width:100%; justify-content:center; font-size:1rem; padding:0.85rem;"> | |
| {% if existing %}✏️ UPDATE PREDICTION{% else %}🚀 SUBMIT PREDICTION{% endif %} | |
| </button> | |
| {% if existing %} | |
| <div style="text-align:center; margin-top:0.75rem; font-size:0.8rem; color:var(--muted);"> | |
| Last updated: {{ existing.updated_at[:16] }} | |
| </div> | |
| {% endif %} | |
| </form> | |
| </div> | |
| {% elif match.status == 'upcoming' and not match.is_match_today %} | |
| <div class="card" style="border-color:rgba(59,130,246,0.25); text-align:center; padding:2rem;"> | |
| <div style="font-size:2.5rem; margin-bottom:1rem;">📆</div> | |
| <div style="font-size:1.15rem; font-weight:700; color:var(--blue);">Predictions open on match day</div> | |
| <div style="color:var(--muted2); margin-top:0.6rem; font-size:0.92rem;">This fixture is on <strong>{{ match.match_date|format_date }}</strong>. Come back that day to submit or edit your pick (until the scheduled start).</div> | |
| {% if existing %} | |
| <div style="margin-top:1.5rem; padding:1rem; background:var(--bg3); border-radius:8px; text-align:left;"> | |
| <div style="font-size:0.8rem; color:var(--muted2); margin-bottom:0.5rem;">YOUR SAVED PICK (from match day)</div> | |
| <div style="font-weight:700; color:var(--orange);">{{ existing.predicted_winner }}</div> | |
| {% if existing.predicted_motm %}<div style="font-size:0.85rem; color:var(--muted2);">⭐ {{ existing.predicted_motm }}</div>{% endif %} | |
| <div style="font-size:0.85rem; color:var(--muted2); margin-top:0.3rem;">Bid: <strong style="color:var(--gold);">{{ '%.0f'|format(existing.bid_amount) }}</strong> pts</div> | |
| </div> | |
| {% endif %} | |
| </div> | |
| {% elif match.locked or match.status == 'locked' %} | |
| <div class="card" style="border-color:rgba(251,191,36,0.3); text-align:center; padding:2rem;"> | |
| <div style="font-size:3rem; margin-bottom:1rem;">🔒</div> | |
| <div style="font-size:1.2rem; font-weight:700; color:var(--gold);">Predictions Locked</div> | |
| <div style="color:var(--muted2); margin-top:0.5rem; font-size:0.9rem;">The prediction window for this match is closed.</div> | |
| {% if existing %} | |
| <div style="margin-top:1.5rem; padding:1rem; background:var(--bg3); border-radius:8px; text-align:left;"> | |
| <div style="font-size:0.8rem; color:var(--muted2); margin-bottom:0.5rem;">YOUR PREDICTION</div> | |
| <div style="font-weight:700; color:var(--orange);">{{ existing.predicted_winner }}</div> | |
| {% if existing.predicted_motm %}<div style="font-size:0.85rem; color:var(--muted2);">⭐ {{ existing.predicted_motm }}</div>{% endif %} | |
| <div style="font-size:0.85rem; color:var(--muted2); margin-top:0.3rem;">Bid: <strong style="color:var(--gold);">{{ '%.0f'|format(existing.bid_amount) }}</strong> pts</div> | |
| </div> | |
| {% else %} | |
| <div style="margin-top:1rem; color:var(--muted); font-size:0.9rem;">You didn't submit a prediction for this match.</div> | |
| {% endif %} | |
| </div> | |
| {% else %} | |
| <div class="card" style="text-align:center; padding:2rem; color:var(--muted2);"> | |
| <div style="font-size:2rem; margin-bottom:0.75rem;">🏏</div> | |
| <div>Predictions aren't available for this match state (<span class="mono">{{ match.status }}</span>).</div> | |
| </div> | |
| {% endif %} | |
| </div> | |
| {% if match.can_predict %} | |
| <script> | |
| const CORRECT_MOTM = {{ points_config.correct_motm }}; | |
| const WRONG_MOTM = {{ points_config.wrong_motm }}; | |
| const MOTM_BY_WINNER = {{ { match.team1: squads.team1, match.team2: squads.team2 } | tojson }}; | |
| const INITIAL_MOTM = {{ (existing.predicted_motm if existing and existing.predicted_motm else '') | tojson }}; | |
| function fillMotmOptionsForWinner(teamName, restoreSavedMotm) { | |
| const sel = document.getElementById('predicted_motm'); | |
| const hint = document.getElementById('motmEmptyHint'); | |
| if (!sel) return; | |
| const players = (MOTM_BY_WINNER && MOTM_BY_WINNER[teamName]) ? MOTM_BY_WINNER[teamName] : []; | |
| let keepValue = ''; | |
| if (restoreSavedMotm && INITIAL_MOTM && players.indexOf(INITIAL_MOTM) !== -1) { | |
| keepValue = INITIAL_MOTM; | |
| } | |
| sel.innerHTML = ''; | |
| const ph = document.createElement('option'); | |
| ph.value = ''; | |
| ph.textContent = players.length ? '— Choose Man of the Match —' : '— No squad for this team —'; | |
| sel.appendChild(ph); | |
| for (let i = 0; i < players.length; i++) { | |
| const o = document.createElement('option'); | |
| o.value = players[i]; | |
| o.textContent = players[i]; | |
| sel.appendChild(o); | |
| } | |
| if (hint) hint.style.display = players.length ? 'none' : 'block'; | |
| if (players.length) { | |
| sel.disabled = false; | |
| sel.required = true; | |
| if (keepValue) sel.value = keepValue; | |
| } else { | |
| sel.disabled = true; | |
| sel.required = false; | |
| sel.value = ''; | |
| } | |
| updateBidDisplay(document.getElementById('bidInput')?.value || 0); | |
| } | |
| function onWinnerChange(radio) { | |
| styleTeamPickCard(radio); | |
| fillMotmOptionsForWinner(radio.value, false); | |
| } | |
| function styleTeamPickCard(radio) { | |
| document.querySelectorAll('.team-pick-card').forEach(el => { | |
| el.style.borderColor = 'var(--border)'; | |
| el.style.background = ''; | |
| }); | |
| const label = radio.closest('.team-pick-label'); | |
| if (label) { | |
| const card = label.querySelector('.team-pick-card'); | |
| if (card) { | |
| const inner = card.querySelector('[style*="font-family"]'); | |
| const color = (inner && inner.style.color) ? inner.style.color : 'var(--orange)'; | |
| card.style.borderColor = color; | |
| if (color.indexOf('rgb(') === 0) { | |
| card.style.background = color.replace('rgb', 'rgba').replace(')', ', 0.1)'); | |
| } else if (color.charAt(0) === '#') { | |
| card.style.background = color.length === 7 ? color + '22' : color; | |
| } else { | |
| card.style.background = 'rgba(249,115,22,0.1)'; | |
| } | |
| } | |
| } | |
| } | |
| (function initMotmFromWinner() { | |
| const checked = document.querySelector('input[name="predicted_winner"]:checked'); | |
| if (checked) { | |
| styleTeamPickCard(checked); | |
| fillMotmOptionsForWinner(checked.value, true); | |
| } | |
| document.querySelectorAll('input[name="predicted_winner"]').forEach((r) => { | |
| r.addEventListener('change', function () { onWinnerChange(this); }); | |
| }); | |
| const motmSel = document.getElementById('predicted_motm'); | |
| motmSel?.addEventListener('change', () => { | |
| updateBidDisplay(document.getElementById('bidInput')?.value || 0); | |
| }); | |
| })(); | |
| function setBid(val) { | |
| const input = document.getElementById('bidInput'); | |
| input.value = val; | |
| updateBidDisplay(val); | |
| } | |
| function updateBidDisplay(val) { | |
| const bid = parseInt(val) || 0; | |
| document.getElementById('winAmount').textContent = '+' + bid; | |
| document.getElementById('loseAmount').textContent = '−' + bid; | |
| const mt = document.getElementById('predicted_motm'); | |
| const hasMOTM = mt && !mt.disabled && (mt.value || '').trim(); | |
| const dash = '—'; | |
| const bestEl = document.getElementById('bestCase'); | |
| const winWrongEl = document.getElementById('winWrongMotm'); | |
| const loseCorrEl = document.getElementById('loseCorrectMotm'); | |
| const loseWrongEl = document.getElementById('loseWrongMotm'); | |
| if (!hasMOTM) { | |
| if (bestEl) bestEl.textContent = dash; | |
| if (winWrongEl) winWrongEl.textContent = dash; | |
| if (loseCorrEl) loseCorrEl.textContent = dash; | |
| if (loseWrongEl) loseWrongEl.textContent = dash; | |
| return; | |
| } | |
| const fmt = (n) => (n >= 0 ? '+' : '') + n + ' pts'; | |
| if (bestEl) bestEl.textContent = fmt(bid + CORRECT_MOTM); | |
| if (winWrongEl) { | |
| const w = bid + WRONG_MOTM; | |
| winWrongEl.textContent = fmt(w); | |
| winWrongEl.className = w >= 0 ? 'text-green' : 'text-red'; | |
| } | |
| if (loseCorrEl) { | |
| const x = -bid + CORRECT_MOTM; | |
| loseCorrEl.textContent = fmt(x); | |
| loseCorrEl.style.color = x >= 0 ? 'var(--green)' : 'var(--red)'; | |
| } | |
| if (loseWrongEl) loseWrongEl.textContent = fmt(-bid + WRONG_MOTM); | |
| } | |
| // Init | |
| updateBidDisplay(document.getElementById('bidInput')?.value || 0); | |
| </script> | |
| {% endif %} | |
| {% endblock %} | |