fix player win % and visual overhaul
This commit is contained in:
16 files changed
+3123
-1490
No files matched your search
+300
-107
@@ -1,133 +1,326 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Leagues · myboker.org{% endblock %}
|
||||
{% block page_class %}page--session page--leagues{% endblock %}
|
||||
{% block content %}
|
||||
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h1 class="page-header__title">Your leagues</h1>
|
||||
{% if leagues %}
|
||||
<p class="page-header__sub">
|
||||
{{ page_summary.league_count }} league{{ 's' if page_summary.league_count != 1 else '' }}
|
||||
{% if page_summary.live_count > 0 %} · <span style="color:var(--pos);">{{ page_summary.live_count }} live</span>{% endif %}
|
||||
· {{ page_summary.sessions_count }} session{{ 's' if page_summary.sessions_count != 1 else '' }}
|
||||
</p>
|
||||
{% endif %}
|
||||
<div class="li-page-header">
|
||||
<div class="li-page-header__left">
|
||||
<p class="eyebrow" style="margin:0 0 6px;">Your account</p>
|
||||
<h1 class="li-page-header__title">Your leagues</h1>
|
||||
</div>
|
||||
<div class="page-header__actions">
|
||||
<a class="btn btn--primary btn--sm" href="{{ url_for('leagues.new') }}">New league</a>
|
||||
<div class="li-page-header__stats">
|
||||
<div class="li-header-stat">
|
||||
<span class="li-header-stat__label">Leagues</span>
|
||||
<strong class="li-header-stat__val">{{ page_summary.league_count }}</strong>
|
||||
</div>
|
||||
<div class="li-header-stat">
|
||||
<span class="li-header-stat__label">Live now</span>
|
||||
<strong class="li-header-stat__val {{ 'num-pos' if page_summary.live_count > 0 else '' }}">{{ page_summary.live_count }}</strong>
|
||||
</div>
|
||||
<div class="li-header-stat">
|
||||
<span class="li-header-stat__label">Sessions</span>
|
||||
<strong class="li-header-stat__val">{{ page_summary.sessions_count }}</strong>
|
||||
</div>
|
||||
<div class="li-header-stat">
|
||||
<span class="li-header-stat__label">Total paid out</span>
|
||||
<strong class="li-header-stat__val">{{ page_summary.total_paid_out_cents | money }}</strong>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if leagues %}
|
||||
<div class="leagues-split">
|
||||
|
||||
<div class="li-board">
|
||||
{% for item in leagues %}
|
||||
{% set league = item.league %}
|
||||
{% set membership = item.membership %}
|
||||
<article class="li-card{% if item.live_session %} li-card--live{% endif %}">
|
||||
<aside class="leagues-split__form">
|
||||
<form class="li-new-form" method="post" action="{{ url_for('leagues.index') }}" id="newLeagueForm">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
<p class="eyebrow" style="margin:0 0 4px;">League setup</p>
|
||||
<h2 class="li-new-form__title">New league</h2>
|
||||
|
||||
<div class="li-card__aside">
|
||||
<span class="li-card__initial">{{ league.name[0]|upper }}</span>
|
||||
<div class="li-new-form__field">
|
||||
<label class="li-new-form__label" for="lf-name">Name</label>
|
||||
<input class="li-new-form__input" id="lf-name" type="text" name="name" value="{{ form.name or '' }}" placeholder="Friday Poker" required>
|
||||
</div>
|
||||
|
||||
<div class="li-new-form__field">
|
||||
<label class="li-new-form__label" for="lf-desc">Description <span class="form-opt-hint">optional</span></label>
|
||||
<textarea class="li-new-form__input li-new-form__textarea" id="lf-desc" name="description" placeholder="Private home league">{{ form.description or '' }}</textarea>
|
||||
</div>
|
||||
|
||||
<div class="li-new-form__field">
|
||||
<span class="li-new-form__label">Visibility</span>
|
||||
<div class="li-visibility-toggle">
|
||||
<button type="button" class="li-vis-btn {% if (form.visibility or 'private') == 'private' %}is-on{% endif %}" data-vis="private">Private</button>
|
||||
<button type="button" class="li-vis-btn {% if (form.visibility or 'private') == 'public' %}is-on{% endif %}" data-vis="public">Public</button>
|
||||
</div>
|
||||
<input type="hidden" name="visibility" id="lf-visibility" value="{{ form.visibility or 'private' }}">
|
||||
<p class="li-new-form__hint" id="visHint">
|
||||
{% if (form.visibility or 'private') == 'public' %}Visible in Explore. Anyone can view.{% else %}Invite-only. Nothing appears in Explore.{% endif %}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="li-new-form__field">
|
||||
<span class="li-new-form__label">Default buy-in</span>
|
||||
<div class="li-preset-row">
|
||||
<button type="button" class="li-preset-btn" data-field="buyin" data-val="5">$5</button>
|
||||
<button type="button" class="li-preset-btn" data-field="buyin" data-val="10">$10</button>
|
||||
<button type="button" class="li-preset-btn" data-field="buyin" data-val="20">$20</button>
|
||||
<button type="button" class="li-preset-btn" data-field="buyin" data-val="50">$50</button>
|
||||
<input class="li-preset-custom" type="number" name="default_buyin_dollars" id="lf-buyin" min="0" step="0.01" placeholder="Custom" value="{{ form.default_buyin_dollars or '' }}">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="li-new-form__field">
|
||||
<span class="li-new-form__label">Default rebuy</span>
|
||||
<div class="li-preset-row">
|
||||
<button type="button" class="li-preset-btn" data-field="rebuy" data-val="5">$5</button>
|
||||
<button type="button" class="li-preset-btn" data-field="rebuy" data-val="10">$10</button>
|
||||
<button type="button" class="li-preset-btn" data-field="rebuy" data-val="20">$20</button>
|
||||
<button type="button" class="li-preset-btn" data-field="rebuy" data-val="50">$50</button>
|
||||
<input class="li-preset-custom" type="number" name="default_rebuy_dollars" id="lf-rebuy" min="0" step="0.01" placeholder="Custom" value="{{ form.default_rebuy_dollars or '' }}">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="li-new-form__two-col">
|
||||
<div class="li-new-form__field">
|
||||
<label class="li-new-form__label" for="lf-min-sessions">Sessions to rank <span class="form-opt-hint">optional</span></label>
|
||||
<input class="li-new-form__input" id="lf-min-sessions" type="number" name="eligible_min_sessions" min="1" step="1" placeholder="3 (default)" value="{{ form.eligible_min_sessions or '' }}">
|
||||
<p class="li-new-form__hint">Min sessions to appear on leaderboard.</p>
|
||||
</div>
|
||||
<div class="li-new-form__field">
|
||||
<label class="li-new-form__label" for="lf-break-even">Break-even threshold <span class="form-opt-hint">optional</span></label>
|
||||
<div class="li-input-prefix-wrap">
|
||||
<span class="li-input-prefix">$</span>
|
||||
<input class="li-new-form__input li-input-has-prefix" id="lf-break-even" type="number" name="break_even_dollars" min="0" step="0.01" placeholder="1.00 (default)" value="{{ form.break_even_dollars or '' }}">
|
||||
</div>
|
||||
<p class="li-new-form__hint">Net within this = break even.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button class="li-new-form__submit" type="submit">Create league</button>
|
||||
</form>
|
||||
</aside>
|
||||
|
||||
<section class="leagues-split__list">
|
||||
<div class="li-toolbar">
|
||||
<input class="li-search" type="search" id="liSearch" placeholder="Search leagues…" autocomplete="off">
|
||||
<div class="li-filter-pills">
|
||||
<button class="li-filter-btn is-on" data-filter="all">All</button>
|
||||
<button class="li-filter-btn" data-filter="live">Live</button>
|
||||
<button class="li-filter-btn" data-filter="owner">Owner</button>
|
||||
<button class="li-filter-btn" data-filter="public">Public</button>
|
||||
</div>
|
||||
<div class="li-sort-tabs">
|
||||
<span class="li-sort-label">Sort</span>
|
||||
<button class="li-sort-btn is-on" data-sort="activity">Activity</button>
|
||||
<button class="li-sort-btn" data-sort="money">Money</button>
|
||||
<button class="li-sort-btn" data-sort="name">Name</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="li-card__body">
|
||||
<div class="li-card__head">
|
||||
<div class="li-card__info">
|
||||
{% if leagues %}
|
||||
<div class="li-board" id="liBoard">
|
||||
{% for item in leagues %}
|
||||
{% set league = item.league %}
|
||||
{% set membership = item.membership %}
|
||||
<article class="li-card{% if item.live_session %} li-card--live{% endif %}"
|
||||
data-name="{{ league.name | lower }}"
|
||||
data-live="{{ '1' if item.live_session else '0' }}"
|
||||
data-role="{{ membership.role }}"
|
||||
data-visibility="{{ league.visibility }}"
|
||||
data-sessions="{{ item.sessions_count }}"
|
||||
data-paid="{{ item.cash_paid_out_cents }}"
|
||||
data-name-raw="{{ league.name }}">
|
||||
|
||||
<div class="li-card__top">
|
||||
<div class="li-card__name-row">
|
||||
<a class="li-card__name" href="{{ url_for('leagues.dashboard', league_ref=league.url_ref) }}">{{ league.name }}</a>
|
||||
{% if item.live_session %}
|
||||
<span class="live-badge">{{ session_label(item.live_session) }}</span>
|
||||
<span class="li-pill li-pill--live">Live</span>
|
||||
{% else %}
|
||||
<span class="status-pill status-pill--closed">idle</span>
|
||||
<span class="li-pill li-pill--idle">Idle</span>
|
||||
{% endif %}
|
||||
{% if item.latest_session %}<span class="li-card__season">{{ session_label(item.latest_session) }}</span>{% endif %}
|
||||
</div>
|
||||
<div style="display:flex;align-items:center;gap:8px;flex-shrink:0;">
|
||||
<span class="li-badge">{{ membership.role }}</span>
|
||||
<span class="li-badge">{{ league.visibility }}</span>
|
||||
<a class="li-open-btn" href="{{ url_for('leagues.dashboard', league_ref=league.url_ref) }}">Open</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="li-card__body">
|
||||
<div class="li-card__stats-col">
|
||||
<span class="li-stat-label">Players</span>
|
||||
<strong class="li-stat-num">{{ item.players_count }}</strong>
|
||||
</div>
|
||||
<div class="li-card__stats-col">
|
||||
<span class="li-stat-label">Sessions</span>
|
||||
<strong class="li-stat-num">{{ item.sessions_count }}</strong>
|
||||
</div>
|
||||
<div class="li-card__stats-col">
|
||||
<span class="li-stat-label">Paid out</span>
|
||||
<strong class="li-stat-num">{{ item.cash_paid_out_cents | money }}</strong>
|
||||
</div>
|
||||
<div class="li-card__stats-col">
|
||||
<span class="li-stat-label">Open</span>
|
||||
<strong class="li-stat-num {{ 'num-neg' if item.open_items_cents > 0 else 'num-muted' }}">{{ item.open_items_cents | money }}</strong>
|
||||
</div>
|
||||
{% if item.top_players %}
|
||||
<div class="li-card__leaders-col">
|
||||
<span class="li-stat-label">Leaders</span>
|
||||
<div class="li-leaders-list">
|
||||
{% for player in item.top_players %}
|
||||
<div class="li-leader-row">
|
||||
<span class="li-leader-rank">#{{ loop.index }}</span>
|
||||
<span class="li-leader-name">{{ player.player_name }}</span>
|
||||
<strong class="li-leader-net {{ 'num-pos' if player.total_net_cents > 0 else 'num-neg' if player.total_net_cents < 0 else 'num-muted' }}">
|
||||
{{ '+' if player.total_net_cents > 0 else '' }}{{ player.total_net_cents | money }}
|
||||
</strong>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="li-card__footer">
|
||||
<span class="li-card__footer-meta">
|
||||
{% if item.live_session %}Live now · table open{% else %}{% if item.latest_session %}Last played {{ session_label(item.latest_session) }}{% else %}No sessions yet{% endif %}{% endif %}
|
||||
</span>
|
||||
<div class="li-card__footer-links">
|
||||
<a class="li-footer-link" href="{{ url_for('leagues.sessions', league_ref=league.url_ref) }}">Sessions</a>
|
||||
<a class="li-footer-link" href="{{ url_for('leagues.leaderboard', league_ref=league.url_ref) }}">Leaderboard</a>
|
||||
{% if membership.role in ('owner', 'manager') %}
|
||||
<a class="li-footer-link" href="{{ url_for('leagues.ledger', league_ref=league.url_ref) }}">Ledger</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="li-card__chips">
|
||||
<span class="db-chip db-chip--accent">{{ membership.role }}</span>
|
||||
<span class="db-chip {% if league.visibility == 'public' %}db-chip--public{% endif %}">
|
||||
{% if league.visibility == 'public' %}
|
||||
<svg width="10" height="10" viewBox="0 0 20 20" fill="none" aria-hidden="true">
|
||||
<circle cx="10" cy="10" r="8" stroke="currentColor" stroke-width="1.6"/>
|
||||
<path d="M10 2c-2.5 3-2.5 13 0 16" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" fill="none"/>
|
||||
<path d="M10 2c2.5 3 2.5 13 0 16" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" fill="none"/>
|
||||
<path d="M2.5 10h15" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"/>
|
||||
</svg>
|
||||
public
|
||||
{% else %}
|
||||
<svg width="9" height="11" viewBox="0 0 10 12" fill="none" aria-hidden="true">
|
||||
<rect x="1" y="5" width="8" height="6.5" rx="1.5" stroke="currentColor" stroke-width="1.5"/>
|
||||
<path d="M3 5V3.5A2 2 0 0 1 7 3.5V5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
|
||||
</svg>
|
||||
private
|
||||
{% endif %}
|
||||
</span>
|
||||
</div>
|
||||
{% if league.description %}
|
||||
<p class="li-card__desc">{{ league.description }}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="li-card__stats">
|
||||
<div class="li-stat">
|
||||
<span>Players</span>
|
||||
<strong>{{ item.players_count }}</strong>
|
||||
</div>
|
||||
<div class="li-stat">
|
||||
<span>Sessions</span>
|
||||
<strong>{{ item.sessions_count }}</strong>
|
||||
</div>
|
||||
<div class="li-stat">
|
||||
<span>Paid out</span>
|
||||
<strong>{{ item.cash_paid_out_cents | money }}</strong>
|
||||
</div>
|
||||
<div class="li-stat">
|
||||
<span>Open</span>
|
||||
<strong class="{{ 'num-neg' if item.open_items_cents > 0 else 'num-muted' }}">{{ item.open_items_cents | money }}</strong>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if item.top_players %}
|
||||
<div class="li-card__leaders">
|
||||
<span class="li-card__leaders-label">Leaders</span>
|
||||
<div class="li-podium">
|
||||
{% for player in item.top_players %}
|
||||
<div class="li-podium-item li-podium-item--{{ loop.index }}">
|
||||
<span class="li-podium-rank">#{{ loop.index }}</span>
|
||||
<span class="li-podium-name">{{ player.player_name }}</span>
|
||||
<strong class="li-podium-net {{ 'num-pos' if player.total_net_cents > 0 else 'num-neg' if player.total_net_cents < 0 else 'num-muted' }}">
|
||||
{{ '+' if player.total_net_cents > 0 else '' }}{{ player.total_net_cents | money }}
|
||||
</strong>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="li-card__footer">
|
||||
<span class="li-card__meta">
|
||||
{% if item.latest_session %}Latest {{ session_label(item.latest_session) }}{% elif not item.top_players %}No sessions yet{% endif %}
|
||||
</span>
|
||||
<div class="li-card__actions">
|
||||
<a class="btn btn--primary btn--sm" href="{{ url_for('leagues.dashboard', league_ref=league.url_ref) }}">Open league</a>
|
||||
<a class="li-action-link" href="{{ url_for('leagues.sessions', league_ref=league.url_ref) }}">Sessions</a>
|
||||
<a class="li-action-link" href="{{ url_for('leagues.leaderboard', league_ref=league.url_ref) }}">Leaderboard</a>
|
||||
{% if membership.role in ('owner', 'manager') %}
|
||||
<a class="li-action-link" href="{{ url_for('leagues.ledger', league_ref=league.url_ref) }}">Ledger</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<p class="li-empty-search" id="liEmptySearch" style="display:none;">No leagues match your search or filter.</p>
|
||||
{% else %}
|
||||
<div class="panel empty-panel">
|
||||
<h2 class="panel__title">No leagues yet</h2>
|
||||
<p class="muted-text">Fill in the form to create your first league.</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</section>
|
||||
|
||||
</article>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
{% else %}
|
||||
<div class="panel empty-panel">
|
||||
<h2 class="panel__title">No leagues yet</h2>
|
||||
<p class="muted-text">Create your first league to start adding players, sessions, and ledger events.</p>
|
||||
<a class="btn btn--primary" href="{{ url_for('leagues.new') }}">Create league</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
<script>
|
||||
(function () {
|
||||
// --- Visibility toggle ---
|
||||
var visInput = document.getElementById('lf-visibility');
|
||||
var visHint = document.getElementById('visHint');
|
||||
document.querySelectorAll('.li-vis-btn').forEach(function (btn) {
|
||||
btn.addEventListener('click', function () {
|
||||
document.querySelectorAll('.li-vis-btn').forEach(function (b) { b.classList.remove('is-on'); });
|
||||
this.classList.add('is-on');
|
||||
visInput.value = this.dataset.vis;
|
||||
visHint.textContent = this.dataset.vis === 'public'
|
||||
? 'Visible in Explore. Anyone can view.'
|
||||
: 'Invite-only. Nothing appears in Explore.';
|
||||
});
|
||||
});
|
||||
|
||||
// --- Preset buy-in / rebuy buttons ---
|
||||
document.querySelectorAll('.li-preset-btn').forEach(function (btn) {
|
||||
btn.addEventListener('click', function () {
|
||||
var field = this.dataset.field;
|
||||
var val = this.dataset.val;
|
||||
var input = document.getElementById('lf-' + field);
|
||||
// toggle: clicking active preset clears it
|
||||
var active = this.classList.contains('is-on');
|
||||
document.querySelectorAll('.li-preset-btn[data-field="' + field + '"]').forEach(function (b) {
|
||||
b.classList.remove('is-on');
|
||||
});
|
||||
if (!active) {
|
||||
this.classList.add('is-on');
|
||||
input.value = val;
|
||||
} else {
|
||||
input.value = '';
|
||||
}
|
||||
});
|
||||
});
|
||||
// Sync custom input → deselect preset if user types a custom value
|
||||
['buyin', 'rebuy'].forEach(function (field) {
|
||||
var input = document.getElementById('lf-' + field);
|
||||
if (!input) return;
|
||||
input.addEventListener('input', function () {
|
||||
var v = this.value.trim();
|
||||
document.querySelectorAll('.li-preset-btn[data-field="' + field + '"]').forEach(function (b) {
|
||||
b.classList.toggle('is-on', b.dataset.val === v);
|
||||
});
|
||||
});
|
||||
// Highlight preset if form was repopulated with an error
|
||||
var cur = input.value.trim();
|
||||
if (cur) {
|
||||
document.querySelectorAll('.li-preset-btn[data-field="' + field + '"]').forEach(function (b) {
|
||||
if (b.dataset.val === cur) b.classList.add('is-on');
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// --- Search + filter + sort ---
|
||||
var cards = Array.from(document.querySelectorAll('.li-card'));
|
||||
var board = document.getElementById('liBoard');
|
||||
var empty = document.getElementById('liEmptySearch');
|
||||
var search = document.getElementById('liSearch');
|
||||
var activeFilter = 'all';
|
||||
var activeSort = 'activity';
|
||||
|
||||
function applyAll() {
|
||||
var q = search ? search.value.trim().toLowerCase() : '';
|
||||
var visible = [];
|
||||
cards.forEach(function (c) {
|
||||
var nameMatch = !q || c.dataset.name.includes(q);
|
||||
var filterMatch = true;
|
||||
if (activeFilter === 'live') filterMatch = c.dataset.live === '1';
|
||||
if (activeFilter === 'owner') filterMatch = c.dataset.role === 'owner';
|
||||
if (activeFilter === 'public') filterMatch = c.dataset.visibility === 'public';
|
||||
var show = nameMatch && filterMatch;
|
||||
c.style.display = show ? '' : 'none';
|
||||
if (show) visible.push(c);
|
||||
});
|
||||
// Sort
|
||||
visible.sort(function (a, b) {
|
||||
if (activeSort === 'name') return a.dataset.nameRaw < b.dataset.nameRaw ? -1 : 1;
|
||||
if (activeSort === 'money') return Number(b.dataset.paid) - Number(a.dataset.paid);
|
||||
// activity: live first, then by sessions desc
|
||||
var la = a.dataset.live === '1' ? 1 : 0;
|
||||
var lb = b.dataset.live === '1' ? 1 : 0;
|
||||
if (lb !== la) return lb - la;
|
||||
return Number(b.dataset.sessions) - Number(a.dataset.sessions);
|
||||
});
|
||||
visible.forEach(function (c) { board && board.appendChild(c); });
|
||||
if (empty) empty.style.display = (visible.length === 0) ? 'block' : 'none';
|
||||
}
|
||||
|
||||
if (search) search.addEventListener('input', applyAll);
|
||||
|
||||
document.querySelectorAll('.li-filter-btn').forEach(function (btn) {
|
||||
btn.addEventListener('click', function () {
|
||||
document.querySelectorAll('.li-filter-btn').forEach(function (b) { b.classList.remove('is-on'); });
|
||||
this.classList.add('is-on');
|
||||
activeFilter = this.dataset.filter;
|
||||
applyAll();
|
||||
});
|
||||
});
|
||||
|
||||
document.querySelectorAll('.li-sort-btn').forEach(function (btn) {
|
||||
btn.addEventListener('click', function () {
|
||||
document.querySelectorAll('.li-sort-btn').forEach(function (b) { b.classList.remove('is-on'); });
|
||||
this.classList.add('is-on');
|
||||
activeSort = this.dataset.sort;
|
||||
applyAll();
|
||||
});
|
||||
});
|
||||
|
||||
applyAll();
|
||||
})();
|
||||
</script>
|
||||
|
||||
{% endblock %}
|
||||
Reference in new issue
Block a user