visual redesign: landing, dashboard overview, session calendar (2.3.1)

- Redesign landing page hero with split layout, features section, and in-app session/result demos
- Add fixed sidebar scroll isolation (app-shell overflow lock pattern)
- Redesign leagues index cards with initial badge, podium leader chips, and action links
- Redesign league dashboard: color-coded nav tiles, cash/stats overview strip, session history heatmap calendar
- Dashboard cash stats use real cash only (excludes rollovers and carry-ins)
- Session history calendar: JS-driven full-year grid, year navigation, hover tooltips, full-width responsive layout
- Bump leaderboard tile color from gold to blue (distinct from ledger orange)
- Bump version to 2.3.1
This commit is contained in:
SowinskiBraeden committed 2026-06-26 21:24:00 -07:00
1 parent 56c9a1084c
commit a5cb488b91
22 files changed
+2975 -597

No files matched your search

+1 -1
View File
@@ -9,7 +9,7 @@ DATA_PATH = BASE_DIR / "data" / "entries.csv"
DEFAULT_DATABASE_URL = f"sqlite:///{BASE_DIR / 'data' / 'boker-dev.sqlite3'}" DEFAULT_DATABASE_URL = f"sqlite:///{BASE_DIR / 'data' / 'boker-dev.sqlite3'}"
ELIGIBLE_MIN_SESSIONS = 3 ELIGIBLE_MIN_SESSIONS = 3
APP_VERSION = "2.3.0" APP_VERSION = "2.3.1"
def load_local_env(env_path: Path) -> None: def load_local_env(env_path: Path) -> None:
+45
View File
@@ -222,16 +222,61 @@ def dashboard(league_ref: str):
return redirect(url_for("public.home")) return redirect(url_for("public.home"))
from league_repositories import league_counts, user_has_league_role from league_repositories import league_counts, user_has_league_role
from ledger_repositories import list_event_rows_for_league
league, resp = get_league_with_visibility_gate(league_ref) league, resp = get_league_with_visibility_gate(league_ref)
if resp: if resp:
return resp return resp
is_owner = user_has_league_role(current_user_id() or "", league.id, {"owner"}) is_owner = user_has_league_role(current_user_id() or "", league.id, {"owner"})
all_sessions = build_session_summaries(list_event_rows_for_league(league.id))
closed_sessions = [s for s in all_sessions if not s.is_open]
closed_count = len(closed_sessions)
total_cash_in = sum(s.total_real_cash_in_cents for s in closed_sessions)
total_paid_out = sum(s.total_paid_out_cents for s in closed_sessions)
total_fronts = sum(s.total_front_cents for s in all_sessions)
closed_entries = sum(len(s.entries) for s in closed_sessions)
all_entries = sum(len(s.entries) for s in all_sessions)
# Avg sessions per month since the first session
session_date_strs = [s.session_date[:10] for s in all_sessions if s.session_date]
avg_sessions_per_month = 0.0
if session_date_strs:
first_date = date.fromisoformat(min(session_date_strs))
today_d = date.today()
months_elapsed = (today_d.year - first_date.year) * 12 + (today_d.month - first_date.month) + 1
avg_sessions_per_month = round(len(all_sessions) / months_elapsed, 1)
# Session calendar data: date → count of sessions on that date
sessions_by_date: dict[str, int] = {}
for s in all_sessions:
if s.session_date:
d = s.session_date[:10]
sessions_by_date[d] = sessions_by_date.get(d, 0) + 1
first_session_year = int(min(sessions_by_date)[:4]) if sessions_by_date else date.today().year
cash_stats = {
"total_cash_in": total_cash_in,
"total_paid_out": total_paid_out,
"total_fronts": total_fronts,
"avg_pot": total_cash_in // closed_count if closed_count else 0,
"avg_players": round(all_entries / len(all_sessions)) if all_sessions else 0,
"avg_buyin": sum(s.total_buy_in_cents for s in closed_sessions) // closed_entries if closed_entries else 0,
"biggest_pot": max((s.total_real_cash_in_cents for s in all_sessions), default=0),
"avg_sessions_per_month": avg_sessions_per_month,
"session_map": sessions_by_date,
"first_session_year": first_session_year,
"has_data": closed_count > 0,
"has_sessions": bool(all_sessions),
}
return render_template( return render_template(
"league_dashboard.html", "league_dashboard.html",
league=league, league=league,
counts=league_counts(league.id), counts=league_counts(league.id),
is_owner=is_owner, is_owner=is_owner,
cash_stats=cash_stats,
) )
+1915 -166
View File
File diff suppressed because it is too large. Load diff
+40 -27
View File
@@ -1,34 +1,37 @@
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap');
:root { :root {
/* ---- surfaces (darkest → lightest) ---- */ /* ---- surfaces (darkest → lightest) ---- */
--bg: #0f0f12; --bg: #0a0a0d;
--surface: #16161c; --surface: #111115;
--surface-raised:#1a1a20; --surface-raised:#16161c;
--surface-head: #1c1c24; --surface-head: #18181f;
--surface-sunk: #131318; --surface-sunk: #0d0d11;
--field: #1f1f27; --field: #1a1a22;
--field-alt: #20202a; --field-alt: #1c1c25;
/* ---- borders / dividers ---- */ /* ---- borders / dividers ---- */
--border: #26262f; --border: #22222b;
--border-strong: #2e2e38; --border-strong: #2a2a34;
--border-hi: #34343f; --border-hi: #32323d;
--divider: #1d1d25; --divider: #191921;
--slash: #3a3a45; --slash: #383843;
/* ---- text ---- */ /* ---- text ---- */
--text: #f3f1f8; --text: #f4f2f9;
--text-strong: #ecebf1; --text-strong: #edeaf5;
--text-body: #e7e5ec; --text-body: #e8e6ef;
--text-2: #bcbac6; --text-2: #bdbbc8;
--num: #b0aeba; --num: #b2b0bb;
--muted: #8f8d99; --muted: #8c8a96;
--muted-2: #84828e; --muted-2: #82808c;
--faint: #7d7b87; --faint: #7a7885;
--faint-2: #757380; --faint-2: #72707d;
--faintest: #66646f; --faintest: #64626d;
--faintest-2: #57555f; --faintest-2: #55535e;
/* ---- accent (violet) ---- */ /* ---- accent ---- */
/* To swap brand color later: edit these 7 vars only */
--accent: #9b8cf0; --accent: #9b8cf0;
--accent-ink: #12101e; --accent-ink: #12101e;
--accent-tint: #1f1d2e; --accent-tint: #1f1d2e;
@@ -65,12 +68,22 @@
--line-1: #9b8cf0; --line-2: #6fc093; --line-3: #e0b15c; --line-1: #9b8cf0; --line-2: #6fc093; --line-3: #e0b15c;
--line-4: #cf6f86; --line-5: #8f93c2; --line-4: #cf6f86; --line-5: #8f93c2;
/* ---- elevation shadows ---- */
--shadow-sm: 0 1px 3px rgba(0,0,0,.30), 0 1px 2px rgba(0,0,0,.22);
--shadow-md: 0 4px 16px rgba(0,0,0,.34), 0 2px 6px rgba(0,0,0,.24);
--shadow-lg: 0 12px 36px rgba(0,0,0,.42), 0 4px 12px rgba(0,0,0,.28);
--shadow-xl: 0 24px 64px rgba(0,0,0,.52), 0 8px 24px rgba(0,0,0,.32);
/* ---- glass panel effect ---- */
--glass-border: 1px solid rgba(255,255,255,.055);
--glass-bg: rgba(255,255,255,.02);
/* ---- spacing scale (4pt) ---- */ /* ---- spacing scale (4pt) ---- */
--s1:4px; --s2:8px; --s3:12px; --s4:16px; --s5:20px; --s1:4px; --s2:8px; --s3:12px; --s4:16px; --s5:20px;
--s6:24px; --s8:32px; --s10:40px; --s12:48px; --s6:24px; --s8:32px; --s10:40px; --s12:48px;
/* ---- radii ---- */ /* ---- radii ---- */
--r-pill:6px; --r-sm:9px; --r-md:11px; --r-lg:14px; --r-pill:6px; --r-sm:9px; --r-md:12px; --r-lg:16px;
/* ---- containers ---- */ /* ---- containers ---- */
--w-public:1320px; --w-public:1320px;
@@ -79,6 +92,6 @@
--gutter: clamp(16px, 4vw, 40px); --gutter: clamp(16px, 4vw, 40px);
/* ---- Chart.js backward compat (read as raw strings by JS) ---- */ /* ---- Chart.js backward compat (read as raw strings by JS) ---- */
--text-muted: #bcbac6; --text-muted: #bdbbc8;
--line: #26262f; --line: #22222b;
} }
+1 -1
View File
@@ -1,4 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 26 26"> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 26 26">
<rect width="26" height="26" rx="5" fill="#9b8cf0"/> <rect width="26" height="26" rx="5" fill="#9b8cf0"/>
<path d="M13 5.22 20.78 13 13 20.78 5.22 13Z" fill="#0f0f12"/> <path d="M13 5.22 20.78 13 13 20.78 5.22 13Z" fill="#12101e"/>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 188 B

After

Width:  |  Height:  |  Size: 188 B

+87
View File
@@ -0,0 +1,87 @@
<aside class="sidebar">
<div class="sidebar__header">
<div class="sidebar__badge">{{ league.name[0]|upper }}</div>
<div class="sidebar__meta">
<span class="sidebar__league-name">{{ league.name }}</span>
{% if is_owner is defined and is_owner %}
<span class="sidebar__role">Owner</span>
{% endif %}
</div>
</div>
<nav class="sidebar__nav" aria-label="League navigation">
<a class="sidebar__link {{ 'is-active' if request.endpoint == 'leagues.dashboard' }}"
href="{{ url_for('leagues.dashboard', league_ref=league.url_ref) }}">
<svg viewBox="0 0 16 16" fill="none" aria-hidden="true">
<rect x="1" y="1" width="6" height="6" rx="1.5" stroke="currentColor" stroke-width="1.4"/>
<rect x="9" y="1" width="6" height="6" rx="1.5" stroke="currentColor" stroke-width="1.4"/>
<rect x="1" y="9" width="6" height="6" rx="1.5" stroke="currentColor" stroke-width="1.4"/>
<rect x="9" y="9" width="6" height="6" rx="1.5" stroke="currentColor" stroke-width="1.4"/>
</svg>
<span>Overview</span>
</a>
<a class="sidebar__link {{ 'is-active' if request.endpoint in ('leagues.sessions', 'leagues.session_detail', 'leagues.session_view') }}"
href="{{ url_for('leagues.sessions', league_ref=league.url_ref) }}">
<svg viewBox="0 0 16 16" fill="none" aria-hidden="true">
<rect x="1" y="3" width="14" height="12" rx="2" stroke="currentColor" stroke-width="1.4"/>
<path d="M5 1v4M11 1v4M1 7h14" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"/>
</svg>
<span>Sessions</span>
</a>
<a class="sidebar__link {{ 'is-active' if request.endpoint in ('leagues.players', 'leagues.player_detail') }}"
href="{{ url_for('leagues.players', league_ref=league.url_ref) }}">
<svg viewBox="0 0 16 16" fill="none" aria-hidden="true">
<circle cx="6" cy="5.5" r="2.5" stroke="currentColor" stroke-width="1.4"/>
<path d="M1 14c0-2.761 2.239-5 5-5" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"/>
<circle cx="11.5" cy="5.5" r="2" stroke="currentColor" stroke-width="1.3"/>
<path d="M9 14c0-1.657 1.119-3.084 2.5-3.084S14 12.343 14 14" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"/>
</svg>
<span>Players</span>
</a>
<a class="sidebar__link {{ 'is-active' if request.endpoint == 'leagues.leaderboard' }}"
href="{{ url_for('leagues.leaderboard', league_ref=league.url_ref) }}">
<svg viewBox="0 0 16 16" fill="none" aria-hidden="true">
<rect x="1" y="9" width="4" height="6" rx="1" stroke="currentColor" stroke-width="1.4"/>
<rect x="6" y="5" width="4" height="10" rx="1" stroke="currentColor" stroke-width="1.4"/>
<rect x="11" y="1" width="4" height="14" rx="1" stroke="currentColor" stroke-width="1.4"/>
</svg>
<span>Leaderboard</span>
</a>
{% if is_logged_in %}
<a class="sidebar__link {{ 'is-active' if request.endpoint == 'leagues.ledger' }}"
href="{{ url_for('leagues.ledger', league_ref=league.url_ref) }}">
<svg viewBox="0 0 16 16" fill="none" aria-hidden="true">
<rect x="2" y="1" width="12" height="14" rx="2" stroke="currentColor" stroke-width="1.4"/>
<path d="M5 5h6M5 8h6M5 11h3" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"/>
</svg>
<span>Ledger</span>
</a>
{% endif %}
{% if is_owner is defined and is_owner %}
<a class="sidebar__link {{ 'is-active' if request.endpoint == 'leagues.league_settings' }}"
href="{{ url_for('leagues.league_settings', league_ref=league.url_ref) }}">
<svg viewBox="0 0 16 16" fill="none" aria-hidden="true">
<circle cx="8" cy="8" r="2.5" stroke="currentColor" stroke-width="1.4"/>
<path d="M8 1v2M8 13v2M1 8h2M13 8h2M2.93 2.93l1.41 1.41M11.66 11.66l1.41 1.41M2.93 13.07l1.41-1.41M11.66 4.34l1.41-1.41" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"/>
</svg>
<span>Settings</span>
</a>
{% endif %}
</nav>
<div class="sidebar__footer">
{% if is_logged_in %}
<a class="sidebar__back" href="{{ url_for('leagues.index') }}">
<svg viewBox="0 0 12 12" fill="none" aria-hidden="true">
<path d="M7 2L3 6l4 4" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
All leagues
</a>
{% endif %}
</div>
</aside>
+61 -20
View File
@@ -1,27 +1,68 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block title %}Login · myboker.org{% endblock %} {% block title %}Sign in · myboker.org{% endblock %}
{% block page_class %}page--auth{% endblock %}
{% block content %} {% block content %}
<div class="form-shell narrow"> <div class="auth-split">
<form class="panel form-card" method="post"> <div class="auth-split__form-col">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"> <div class="auth-split__form-inner">
<div> <a class="auth-split__brand" href="{{ url_for('public.home') }}">
<p class="eyebrow">Owner access</p> <div class="auth-split__brand-mark"></div>
<h1 style="font-size:28px;margin-bottom:8px;">Account login</h1> <span class="auth-split__brand-name">myboker<span class="auth-split__brand-tld">.org</span></span>
<p class="muted-text" style="margin-bottom:8px;">Manage leagues, players, sessions, and ledger entries.</p> </a>
<h1 class="auth-split__title">Welcome back.</h1>
<p class="auth-split__sub">Sign in to manage your leagues, sessions, and ledger.</p>
<form method="post">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<div class="auth-field">
<label for="email">Email</label>
<input id="email" type="email" name="email" autocomplete="email"
value="{{ form.email or '' }}" placeholder="you@example.com" required>
</div>
<div class="auth-field">
<label for="password">Password</label>
<input id="password" type="password" name="password"
autocomplete="current-password" placeholder="••••••••" required>
</div>
<div class="auth-links">
<a href="{{ url_for('account.forgot_password') }}">Forgot password?</a>
</div>
<button class="btn btn--primary auth-submit" type="submit">Sign in →</button>
</form>
<p class="auth-alt">No account yet? <a href="{{ url_for('account.register') }}">Create one</a></p>
</div> </div>
<label> </div>
<span>Email</span>
<input type="email" name="email" autocomplete="email" value="{{ form.email or '' }}" required> <div class="auth-split__visual" aria-hidden="true">
</label> <div class="auth-visual__body">
<label> <div class="auth-visual__icon">
<span>Password</span> <svg width="26" height="26" viewBox="0 0 26 26" fill="none">
<input type="password" name="password" autocomplete="current-password" required> <path d="M13 2L22 7.5V18.5L13 24L4 18.5V7.5L13 2Z" stroke="rgba(155,140,240,.65)" stroke-width="1.4" fill="none"/>
</label> <path d="M13 8L18 11V17L13 20L8 17V11L13 8Z" fill="rgba(155,140,240,.25)" stroke="rgba(155,140,240,.55)" stroke-width="1"/>
<button class="btn btn--primary" type="submit">Login</button> </svg>
<a class="btn btn--ghost" href="{{ url_for('account.register') }}">Create account</a> </div>
<a style="font:400 12px/1 var(--font-ui);color:var(--text-3);text-align:center;margin-top:4px;" href="{{ url_for('account.forgot_password') }}">Forgot password?</a> <p class="auth-visual__title">Track every chip.</p>
</form> <p class="auth-visual__sub">Sessions, leaderboards, and debts — all in one place for your private poker league.</p>
<div class="auth-visual__preview">
<div class="auth-visual__preview-kicker">Live leaderboard</div>
<div class="auth-visual__preview-row">
<span>Rivera</span>
<strong style="color:var(--pos);">+$1,420</strong>
</div>
<div class="auth-visual__preview-row">
<span>Chen</span>
<strong style="color:var(--pos);">+$640</strong>
</div>
<div class="auth-visual__preview-row" style="opacity:.5">
<span>Okafor</span>
<strong style="color:var(--neg);">-$210</strong>
</div>
</div>
</div>
</div>
</div> </div>
{% endblock %} {% endblock %}
+63 -22
View File
@@ -1,30 +1,71 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block title %}Create Account · myboker.org{% endblock %} {% block title %}Create Account · myboker.org{% endblock %}
{% block page_class %}page--auth{% endblock %}
{% block content %} {% block content %}
<div class="form-shell narrow"> <div class="auth-split">
<form class="panel form-card" method="post"> <div class="auth-split__form-col">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"> <div class="auth-split__form-inner">
<div> <a class="auth-split__brand" href="{{ url_for('public.home') }}">
<p class="eyebrow">Owner account</p> <div class="auth-split__brand-mark"></div>
<h1 style="font-size:28px;margin-bottom:8px;">Create account</h1> <span class="auth-split__brand-name">myboker<span class="auth-split__brand-tld">.org</span></span>
<p class="muted-text" style="margin-bottom:8px;">Accounts are for league owners and managers.</p> </a>
<h1 class="auth-split__title">Start your league.</h1>
<p class="auth-split__sub">Create an account to manage leagues, players, and sessions.</p>
<form method="post">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<div class="auth-field">
<label for="email">Email</label>
<input id="email" type="email" name="email" autocomplete="email"
value="{{ form.email or '' }}" placeholder="you@example.com" required>
</div>
<div class="auth-field">
<label for="password">Password</label>
<input id="password" type="password" name="password"
autocomplete="new-password" placeholder="At least 8 characters" minlength="8" required>
</div>
<div class="auth-field">
<label for="confirm_password">Confirm password</label>
<input id="confirm_password" type="password" name="confirm_password"
autocomplete="new-password" placeholder="Repeat your password" minlength="8" required>
</div>
<button class="btn btn--primary auth-submit" type="submit">Create account →</button>
</form>
<p class="auth-alt">Already have an account? <a href="{{ url_for('account.login') }}">Sign in</a></p>
</div> </div>
<label> </div>
<span>Email</span>
<input type="email" name="email" autocomplete="email" value="{{ form.email or '' }}" required> <div class="auth-split__visual" aria-hidden="true">
</label> <div class="auth-visual__body">
<label> <div class="auth-visual__icon">
<span>Password</span> <svg width="26" height="26" viewBox="0 0 26 26" fill="none">
<input type="password" name="password" autocomplete="new-password" minlength="8" required> <rect x="3" y="5" width="20" height="16" rx="3" stroke="rgba(155,140,240,.65)" stroke-width="1.4" fill="none"/>
</label> <path d="M3 10h20" stroke="rgba(155,140,240,.45)" stroke-width="1.2"/>
<label> <path d="M8 15h4M8 18h6" stroke="rgba(155,140,240,.55)" stroke-width="1.2" stroke-linecap="round"/>
<span>Confirm password</span> </svg>
<input type="password" name="confirm_password" autocomplete="new-password" minlength="8" required> </div>
</label> <p class="auth-visual__title">Free to start.</p>
<button class="btn btn--primary" type="submit">Create account</button> <p class="auth-visual__sub">No credit card, no monthly fees, no payment processor taking a cut of your buy-ins.</p>
<a class="btn btn--ghost" href="{{ url_for('account.login') }}">Login instead</a> <div class="auth-visual__preview">
</form> <div class="auth-visual__preview-kicker">What you get</div>
<div class="auth-visual__preview-row">
<span>Multiple leagues</span>
<strong style="color:var(--pos);"></strong>
</div>
<div class="auth-visual__preview-row">
<span>Full ledger history</span>
<strong style="color:var(--pos);"></strong>
</div>
<div class="auth-visual__preview-row">
<span>Platform fee</span>
<strong style="color:var(--muted);">$0</strong>
</div>
</div>
</div>
</div>
</div> </div>
{% endblock %} {% endblock %}
+7 -8
View File
@@ -2,14 +2,13 @@
{% block title %}Account settings · myboker.org{% endblock %} {% block title %}Account settings · myboker.org{% endblock %}
{% block content %} {% block content %}
<div class="hero" style="padding-bottom:18px;"> <div class="settings-profile">
<p class="hero__kicker">Account</p> <div class="settings-avatar">{{ user.email[0] | upper }}</div>
<h1 class="hero__title">Settings</h1> <div class="settings-profile__meta">
<p class="hero__sub">Manage your email address, password, and account.</p> <h1 class="settings-profile__title">Account Settings</h1>
</div> <p class="settings-profile__email">{{ user.email }}</p>
</div>
<div class="toolbar"> <div style="margin-left:auto;">
<div class="toolbar__left">
<a class="btn btn--ghost btn--sm" href="{{ url_for('leagues.index') }}">My leagues</a> <a class="btn btn--ghost btn--sm" href="{{ url_for('leagues.index') }}">My leagues</a>
</div> </div>
</div> </div>
+18 -5
View File
@@ -15,7 +15,7 @@
<div class="topbar__inner"> <div class="topbar__inner">
<a class="brand" href="{{ url_for('public.home') }}"> <a class="brand" href="{{ url_for('public.home') }}">
<span class="brand__mark"></span> <span class="brand__mark"></span>
<span class="brand__name">myboker.org</span> <span class="brand__name">myboker<span class="brand__tld">.org</span></span>
</a> </a>
<button class="nav-toggle" type="button" aria-controls="mainnav" aria-expanded="false" aria-label="Open navigation"> <button class="nav-toggle" type="button" aria-controls="mainnav" aria-expanded="false" aria-label="Open navigation">
<span></span> <span></span>
@@ -51,8 +51,10 @@
{% endif %} {% endif %}
</div> </div>
</header> </header>
{% block subnav %}{% endblock %} <div class="app-shell">
{% block sidebar %}{% endblock %}
<main class="page {% block page_class %}page--public{% endblock %}"> <main class="page {% block page_class %}page--public{% endblock %}">
{% block subnav %}{% endblock %}
{% with messages = get_flashed_messages(with_categories=true) %} {% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %} {% if messages %}
<div class="flash-stack"> <div class="flash-stack">
@@ -66,11 +68,22 @@
{% block content %}{% endblock %} {% block content %}{% endblock %}
<footer class="site-footer" aria-label="Site footer"> <footer class="site-footer" aria-label="Site footer">
<span>&copy; 2026 myboker.org</span> <div class="site-footer__copy">
<span>Version {{ app_version }}</span> <span>&copy; 2026 <strong>myboker.org</strong></span>
<span><a href="{{ url_for('public.help') }}" style="color:inherit;text-decoration:none;">Help</a> · cedarline.digital</span> <span class="site-footer__version">v{{ app_version }}</span>
</div>
<nav class="site-footer__links" aria-label="Footer links">
<a href="{{ url_for('public.help') }}">Help</a>
<a href="#">Contact</a>
<a href="#">Privacy</a>
<a href="#">Terms</a>
</nav>
<div class="site-footer__brand">
<a href="https://cedarline.digital" target="_blank" rel="noopener">cedarline.digital</a>
</div>
</footer> </footer>
</main> </main>
</div><!-- /.app-shell -->
<script> <script>
(() => { (() => {
const toggle = document.querySelector('.nav-toggle'); const toggle = document.querySelector('.nav-toggle');
+29 -25
View File
@@ -2,58 +2,62 @@
{% block title %}Explore leagues · myboker.org{% endblock %} {% block title %}Explore leagues · myboker.org{% endblock %}
{% block content %} {% block content %}
<div class="hero" style="padding-bottom:8px;"> <div class="explore-header">
<p class="hero__kicker">Public leagues</p> <p class="explore-header__eyebrow">Public leagues</p>
<h1 class="hero__title">Explore</h1> <h1 class="explore-header__title">Explore</h1>
<p class="hero__sub">Browse poker leagues that have made their results publicly visible.</p> <p class="explore-header__sub">Browse poker leagues that have made their results publicly visible.</p>
</div> </div>
<form method="get" action="{{ url_for('public.explore') }}" class="explore-search"> <form method="get" action="{{ url_for('public.explore') }}" class="explore-search-row">
<input class="field-input explore-search__input" type="text" name="q" value="{{ q }}" placeholder="Search by league name…"> <input class="field-input" type="text" name="q" value="{{ q }}" placeholder="Search by league name…" autocomplete="off">
<button class="btn btn--primary" type="submit">Search</button> <button class="btn btn--primary" type="submit">Search</button>
{% if q %}<a class="btn btn--ghost" href="{{ url_for('public.explore') }}">Clear</a>{% endif %} {% if q %}<a class="btn btn--ghost" href="{{ url_for('public.explore') }}">Clear</a>{% endif %}
</form> </form>
{% if leagues %} {% if leagues %}
<div class="explore-grid"> <div class="explore-cards">
{% for league in leagues %} {% for league in leagues %}
{% set c = counts.get(league.id, {}) %} {% set c = counts.get(league.id, {}) %}
<a class="explore-card" href="{{ url_for('leagues.dashboard', league_ref=league.url_ref) }}"> <a class="ex-card" href="{{ url_for('leagues.dashboard', league_ref=league.url_ref) }}">
<div class="explore-card__head"> <div class="ex-card__initial">{{ league.name[0] | upper }}</div>
<h2 class="explore-card__name">{{ league.name }}</h2> <h2 class="ex-card__name">{{ league.name }}</h2>
{% if league.description %} {% if league.description %}
<p class="explore-card__desc">{{ league.description }}</p> <p class="ex-card__desc">{{ league.description }}</p>
{% endif %} {% else %}
</div> <p class="ex-card__desc" style="opacity:.35;">No description</p>
<div class="explore-card__stats"> {% endif %}
<div class="explore-card__stat"> <div class="ex-card__stats">
<div class="ex-card__stat">
<span class="kicker">Players</span> <span class="kicker">Players</span>
<strong>{{ c.get('players', 0) }}</strong> <strong>{{ c.get('players', 0) }}</strong>
</div> </div>
<div class="explore-card__stat"> <div class="ex-card__stat">
<span class="kicker">Sessions</span> <span class="kicker">Sessions</span>
<strong>{{ c.get('sessions', 0) }}</strong> <strong>{{ c.get('sessions', 0) }}</strong>
</div> </div>
<div class="explore-card__stat"> {% if c.get('open_sessions', 0) > 0 %}
<span class="kicker">Open</span> <div class="ex-card__stat">
<strong>{{ c.get('open_sessions', 0) }}</strong> <span class="kicker">Live</span>
<strong style="color:var(--pos);">{{ c.get('open_sessions', 0) }}</strong>
</div> </div>
{% endif %}
</div> </div>
<div class="explore-card__footer"> <div class="ex-card__footer">
<span class="explore-card__link">View league →</span> <span class="ex-card__link">View league →</span>
</div> </div>
</a> </a>
{% endfor %} {% endfor %}
</div> </div>
{% elif q %} {% elif q %}
<div class="empty-panel" style="max-width:480px;"> <div class="panel empty-panel" style="max-width:480px;">
<h3 class="panel__title">No results for "{{ q }}"</h3> <h3 class="panel__title">No results for "{{ q }}"</h3>
<p class="muted-text">Try a shorter search or browse all leagues.</p> <p class="muted-text">Try a shorter search or browse all leagues.</p>
<a class="btn btn--ghost btn--sm" href="{{ url_for('public.explore') }}">Browse all</a>
</div> </div>
{% else %} {% else %}
<div class="empty-panel" style="max-width:480px;"> <div class="panel empty-panel" style="max-width:480px;">
<h3 class="panel__title">No public leagues yet</h3> <h3 class="panel__title">No public leagues yet</h3>
<p class="muted-text">League owners can set their league to public in settings.</p> <p class="muted-text">League owners can set their league to public in league settings.</p>
</div> </div>
{% endif %} {% endif %}
+283 -137
View File
@@ -1,157 +1,303 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block title %}myboker.org · Poker league records{% endblock %} {% block title %}myboker.org · Poker league records{% endblock %}
{% block page_class %}page--landing{% endblock %}
{% block content %} {% block content %}
<section class="l-hero"> <div class="lp-hero">
<p class="l-hero__kicker">For private poker leagues</p> <div class="lp-hero__content">
<h1 class="l-hero__h1">Your home game,<br>properly tracked.</h1> <span class="lp-eyebrow">Home poker tracking</span>
<p class="l-hero__sub">Sessions, ledgers, leaderboards, and settlement visibility — built for home groups that want clean records without a payment app or social network in the way.</p> <h1 class="lp-h1">
<div class="l-hero__cta"> <span class="lp-h1__line">Your</span>
{% if is_logged_in %} <span class="lp-h1__line">league,</span>
<a class="btn btn--primary" href="{{ url_for('leagues.index') }}">Open leagues</a> <span class="lp-h1__accent">on record.</span>
{% else %} </h1>
<a class="btn btn--primary" href="{{ url_for('account.register') }}">Create account</a> <p class="lp-sub">Sessions, leaderboards, and settlement tracking for home poker groups. No fees, no payment apps in the way.</p>
<a class="btn btn--ghost" href="{{ url_for('account.login') }}">Sign in</a> <div class="lp-cta">
<a class="btn btn--ghost" href="{{ url_for('public.explore') }}">Explore league stats</a> {% if is_logged_in %}
{% endif %} <a class="btn btn--primary btn--lg" href="{{ url_for('leagues.index') }}">Open my leagues →</a>
</div> {% else %}
</section> <a class="btn btn--primary btn--lg" href="{{ url_for('account.register') }}">Start your league →</a>
<a class="btn btn--ghost btn--lg" href="{{ url_for('account.login') }}">Sign in</a>
<div class="l-preview" aria-hidden="true"> {% endif %}
<div class="l-preview__panel">
<div class="l-preview__head">
<span class="kicker" style="margin:0;">Leaderboard</span>
<span class="l-preview__tag">All-time</span>
</div> </div>
<div class="l-preview__rows"> <div class="lp-trust">
<div class="l-preview__row"> <span>No fees</span>
<span class="l-preview__rank">1</span> <span class="lp-trust__sep">·</span>
<span class="l-preview__player">Rivera</span> <span>Append-only ledger</span>
<span class="l-preview__val l-preview__val--pos">+$1,420</span> <span class="lp-trust__sep">·</span>
</div> <span>Private by default</span>
<div class="l-preview__row"> {% if not is_logged_in %}
<span class="l-preview__rank">2</span> <span class="lp-trust__sep">·</span>
<span class="l-preview__player">Chen</span> <a href="{{ url_for('public.explore') }}" style="color:inherit;text-decoration:underline;text-decoration-color:rgba(255,255,255,.2);">Explore public leagues</a>
<span class="l-preview__val l-preview__val--pos">+$640</span> {% endif %}
</div>
<div class="l-preview__row">
<span class="l-preview__rank">3</span>
<span class="l-preview__player">Okafor</span>
<span class="l-preview__val l-preview__val--neg">-$210</span>
</div>
<div class="l-preview__row l-preview__row--faded">
<span class="l-preview__rank">4</span>
<span class="l-preview__player">Walsh</span>
<span class="l-preview__val l-preview__val--neg">-$850</span>
</div>
</div> </div>
</div> </div>
<div class="l-preview__panel l-preview__panel--featured"> <div class="lp-hero__visual" aria-hidden="true">
<div class="l-preview__head"> <div class="lp-demo__card">
<span class="kicker" style="margin:0;">Session #18</span> <div class="lp-demo__head">
<span class="live-badge">live</span> <div>
</div> <span class="lp-demo__kicker">Friday Night Poker</span>
<div class="l-preview__stats"> <span class="lp-demo__title">Leaderboard · Eligible</span>
<div class="l-preview__stat"> </div>
<span class="kicker" style="margin:0;">Cash in</span> <span class="lp-demo__meta">18 sessions · 5 players</span>
<strong>$1,200</strong>
</div> </div>
<div class="l-preview__stat"> <div class="lp-demo__table">
<span class="kicker" style="margin:0;">Players</span> <div class="lp-demo__thead lp-demo__thead--lb">
<strong>8</strong> <span class="lp-demo__th">#</span>
</div> <span class="lp-demo__th">Player</span>
<div class="l-preview__stat"> <span class="lp-demo__th">Δ</span>
<span class="kicker" style="margin:0;">Open debts</span> <span class="lp-demo__th">Net</span>
<strong style="color:var(--warn);">$180</strong> <span class="lp-demo__th">Win%</span>
</div> <span class="lp-demo__th">ROI</span>
</div> </div>
<div class="l-preview__evts"> <div class="lp-demo__row lp-demo__row--lb">
<div class="l-preview__evt"> <span class="lp-demo__rank lp-demo__rank--1">1</span>
<span class="pill pill-buyin">buy-in</span> <span class="lp-demo__player">Rivera</span>
<span class="l-preview__evt-name">Rivera</span> <span class="lp-demo__delta lp-demo__delta--up">▲1</span>
<span class="l-preview__evt-val">$200</span> <span class="lp-demo__num lp-demo__num--pos">+$1,420</span>
</div> <span class="lp-demo__num lp-demo__num--pos">72%</span>
<div class="l-preview__evt"> <span class="lp-demo__num lp-demo__num--pos">+46%</span>
<span class="pill pill-front">front</span> </div>
<span class="l-preview__evt-name">Chen</span> <div class="lp-demo__row lp-demo__row--lb">
<span class="l-preview__evt-val">$200</span> <span class="lp-demo__rank lp-demo__rank--2">2</span>
</div> <span class="lp-demo__player">Chen</span>
<div class="l-preview__evt"> <span class="lp-demo__delta"></span>
<span class="pill pill-cashout">cashout</span> <span class="lp-demo__num lp-demo__num--pos">+$640</span>
<span class="l-preview__evt-name">Okafor</span> <span class="lp-demo__num lp-demo__num--pos">56%</span>
<span class="l-preview__evt-val">$450</span> <span class="lp-demo__num lp-demo__num--pos">+18%</span>
</div> </div>
</div> <div class="lp-demo__row lp-demo__row--lb">
</div> <span class="lp-demo__rank lp-demo__rank--3">3</span>
<span class="lp-demo__player">Okafor</span>
<div class="l-preview__panel"> <span class="lp-demo__delta lp-demo__delta--down">▼1</span>
<div class="l-preview__head"> <span class="lp-demo__num lp-demo__num--neg">$210</span>
<span class="kicker" style="margin:0;">Open debts</span> <span class="lp-demo__num lp-demo__num--neg">43%</span>
<span class="l-preview__tag">Session #17</span> <span class="lp-demo__num lp-demo__num--neg">7%</span>
</div> </div>
<div class="l-preview__rows"> <div class="lp-demo__row lp-demo__row--lb lp-demo__row--fade">
<div class="l-preview__row"> <span class="lp-demo__rank">4</span>
<span class="l-preview__player">Walsh</span> <span class="lp-demo__player">Walsh</span>
<span class="l-preview__val" style="color:var(--owes);">owes $120</span> <span class="lp-demo__delta"></span>
</div> <span class="lp-demo__num lp-demo__num--neg">$850</span>
<div class="l-preview__row"> <span class="lp-demo__num lp-demo__num--neg">31%</span>
<span class="l-preview__player">Okafor</span> <span class="lp-demo__num lp-demo__num--neg">27%</span>
<span class="l-preview__val" style="color:var(--owes);">owes $60</span> </div>
</div> <div class="lp-demo__row lp-demo__row--lb lp-demo__row--fade2">
<div class="l-preview__row l-preview__row--faded"> <span class="lp-demo__rank">5</span>
<span class="l-preview__player">Chen</span> <span class="lp-demo__player">Novak</span>
<span class="l-preview__val" style="color:var(--pos);">settled</span> <span class="lp-demo__delta lp-demo__delta--down">▼2</span>
</div> <span class="lp-demo__num lp-demo__num--neg">$1,000</span>
<div class="l-preview__row l-preview__row--faded"> <span class="lp-demo__num lp-demo__num--neg">25%</span>
<span class="l-preview__player">Rivera</span> <span class="lp-demo__num lp-demo__num--neg">42%</span>
<span class="l-preview__val" style="color:var(--pos);">settled</span> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<section class="l-features"> <!-- Features -->
<div class="l-feature"> <section class="lp-features">
<div class="l-feature__icon"> <div class="lp-features__inner">
<svg width="22" height="22" viewBox="0 0 22 22" fill="none" aria-hidden="true"> <div class="lp-sec-head">
<rect x="1" y="7" width="14" height="13" rx="2" stroke="currentColor" stroke-width="1.5"/> <span class="lp-eyebrow">What's included</span>
<path d="M5 7V5a2 2 0 0 1 2-2h11a2 2 0 0 1 2 2v11a2 2 0 0 1-2 2h-2" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/> <h2 class="lp-sec-title">No spreadsheets. No Venmo math. No guessing.</h2>
</svg>
</div> </div>
<h3 class="l-feature__title">League workspaces</h3> <div class="lp-feat-grid">
<p class="l-feature__desc">Each league has its own players, sessions, leaderboard, and accounting. One account can manage multiple leagues independently.</p> <div class="lp-feat">
</div> <div class="lp-feat__icon">
<div class="l-feature"> <svg viewBox="0 0 20 20" fill="none" aria-hidden="true">
<div class="l-feature__icon"> <rect x="2" y="4" width="16" height="14" rx="2.5" stroke="currentColor" stroke-width="1.5"/>
<svg width="22" height="22" viewBox="0 0 22 22" fill="none" aria-hidden="true"> <path d="M6 2v4M14 2v4M2 9h16" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
<rect x="3" y="3" width="16" height="16" rx="2" stroke="currentColor" stroke-width="1.5"/> <path d="M6 12.5h3M6 15.5h5" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"/>
<path d="M7 8h8M7 11h6M7 14h4" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/> </svg>
</svg> </div>
<h3 class="lp-feat__title">Game-night sessions</h3>
<p class="lp-feat__desc">Open a session at the start of the night, log buy-ins and cashouts as they happen, then close it when the last player leaves. Multiple sessions on the same date auto-sequence as S1, S2.</p>
</div>
<div class="lp-feat">
<div class="lp-feat__icon">
<svg viewBox="0 0 20 20" fill="none" aria-hidden="true">
<rect x="1.5" y="11" width="4.5" height="7" rx="1.2" stroke="currentColor" stroke-width="1.5"/>
<rect x="7.75" y="7" width="4.5" height="11" rx="1.2" stroke="currentColor" stroke-width="1.5"/>
<rect x="14" y="2" width="4.5" height="16" rx="1.2" stroke="currentColor" stroke-width="1.5"/>
</svg>
</div>
<h3 class="lp-feat__title">Rankings that earn their spot</h3>
<p class="lp-feat__desc">Set a minimum session count so regulars rank and one-time guests go provisional until they qualify. Sort by net, ROI, win rate, or recent form. Every number is derived directly from raw ledger events.</p>
</div>
<div class="lp-feat">
<div class="lp-feat__icon">
<svg viewBox="0 0 20 20" fill="none" aria-hidden="true">
<rect x="3" y="2" width="14" height="16" rx="2" stroke="currentColor" stroke-width="1.5"/>
<path d="M7 7h6M7 10.5h6M7 14h3.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
</svg>
</div>
<h3 class="lp-feat__title">Settlement without the spreadsheet</h3>
<p class="lp-feat__desc">Every event is logged and kept: buy-ins, cashouts, fronts, corrections. Nothing is deleted; mistakes get fixed by appending. When a session closes, the house knows exactly what it holds and who is owed what.</p>
</div>
</div> </div>
<h3 class="l-feature__title">Append-only ledger</h3>
<p class="l-feature__desc">Buy-ins, fronts, cashouts, rollovers, debt repayments, and write-offs stay permanently on record. Events are never deleted.</p>
</div>
<div class="l-feature">
<div class="l-feature__icon">
<svg width="22" height="22" viewBox="0 0 22 22" fill="none" aria-hidden="true">
<path d="M3 17L8 11L13 14L20 6" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M3 20h18" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
</svg>
</div>
<h3 class="l-feature__title">Performance analytics</h3>
<p class="l-feature__desc">Net, ROI, win rate, and all-time cash movement are computed from ledger events. Rankings stay separate from settlement records.</p>
</div>
<div class="l-feature">
<div class="l-feature__icon">
<svg width="22" height="22" viewBox="0 0 22 22" fill="none" aria-hidden="true">
<path d="M7 5h8M15 5l-3-3m3 3l-3 3" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M15 17H7m0 0l3 3m-3-3l3-3" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</div>
<h3 class="l-feature__title">Settlement visibility</h3>
<p class="l-feature__desc">Open debts, player payables, and house receivables are always tracked. Share settlement status with the group, not just final numbers.</p>
</div> </div>
</section> </section>
<!-- App demos -->
<section class="lp-showcase">
<div class="lp-showcase__inner">
<div class="lp-sec-head">
<span class="lp-eyebrow">In the app</span>
<h2 class="lp-sec-title">Sessions and results</h2>
<p class="lp-sec-sub">Every number your group tracks, in one place.</p>
</div>
<div class="lp-showcase__grid" aria-hidden="true">
<!-- Sessions panel -->
<div class="lp-demo__card">
<div class="lp-demo__head">
<div>
<span class="lp-demo__kicker">Friday Night Poker</span>
<span class="lp-demo__title">Sessions</span>
</div>
<span class="lp-demo__meta">6 total</span>
</div>
<div class="lp-sess-list">
<div class="lp-sess-row">
<div class="lp-sess-date">
<span class="lp-sess-date__mon">Jun</span>
<span class="lp-sess-date__day">21</span>
</div>
<div class="lp-sess-info">
<span class="lp-sess-label">Session 1</span>
<span class="lp-sess-meta">5 players · $250 pot</span>
</div>
<span class="lp-sess-pill lp-sess-pill--open">open</span>
</div>
<div class="lp-sess-row">
<div class="lp-sess-date">
<span class="lp-sess-date__mon">Jun</span>
<span class="lp-sess-date__day">14</span>
</div>
<div class="lp-sess-info">
<span class="lp-sess-label">Session 1</span>
<span class="lp-sess-meta">5 players · $300 pot</span>
</div>
<span class="lp-sess-pill lp-sess-pill--closed">closed</span>
</div>
<div class="lp-sess-row">
<div class="lp-sess-date">
<span class="lp-sess-date__mon">Jun</span>
<span class="lp-sess-date__day">7</span>
</div>
<div class="lp-sess-info">
<span class="lp-sess-label">Session 1</span>
<span class="lp-sess-meta">4 players · $200 pot</span>
</div>
<span class="lp-sess-pill lp-sess-pill--closed">closed</span>
</div>
<div class="lp-sess-row lp-sess-row--fade">
<div class="lp-sess-date">
<span class="lp-sess-date__mon">May</span>
<span class="lp-sess-date__day">31</span>
</div>
<div class="lp-sess-info">
<span class="lp-sess-label">Session 1</span>
<span class="lp-sess-meta">6 players · $350 pot</span>
</div>
<span class="lp-sess-pill lp-sess-pill--closed">closed</span>
</div>
<div class="lp-sess-row lp-sess-row--fade2">
<div class="lp-sess-date">
<span class="lp-sess-date__mon">May</span>
<span class="lp-sess-date__day">24</span>
</div>
<div class="lp-sess-info">
<span class="lp-sess-label">Session 1</span>
<span class="lp-sess-meta">5 players · $250 pot</span>
</div>
<span class="lp-sess-pill lp-sess-pill--closed">closed</span>
</div>
</div>
</div>
<!-- Session results panel -->
<div class="lp-demo__card">
<div class="lp-demo__head">
<div>
<span class="lp-demo__kicker">Friday Night Poker</span>
<span class="lp-demo__title">Session #006 · Jun 21</span>
</div>
<span class="lp-demo__meta">5 players · open</span>
</div>
<div class="lp-recon-grid">
<div class="lp-recon-cell">
<span class="lp-recon-label">Cash in</span>
<span class="lp-recon-val">$300</span>
</div>
<div class="lp-recon-cell">
<span class="lp-recon-label">Paid out</span>
<span class="lp-recon-val lp-demo__num--pos">$190</span>
</div>
<div class="lp-recon-cell">
<span class="lp-recon-label">House owes players</span>
<span class="lp-recon-val lp-demo__num--neg">$110</span>
</div>
<div class="lp-recon-cell">
<span class="lp-recon-label">Players owe house</span>
<span class="lp-recon-val">$0</span>
</div>
</div>
<div class="lp-result-thead">
<span>Player</span>
<span>Invested</span>
<span>Net</span>
<span>Open</span>
</div>
<div class="lp-result-row">
<span class="lp-demo__player">Rivera</span>
<span class="lp-demo__num">$50</span>
<span class="lp-demo__num lp-demo__num--pos">+$120</span>
<span class="lp-result-due">due $120</span>
</div>
<div class="lp-result-row">
<span class="lp-demo__player">Chen</span>
<span class="lp-demo__num">$100</span>
<span class="lp-demo__num lp-demo__num--pos">+$90</span>
<span class="lp-result-due">due $90</span>
</div>
<div class="lp-result-row">
<span class="lp-demo__player">Walsh</span>
<span class="lp-demo__num">$50</span>
<span class="lp-demo__num lp-demo__num--neg">-$50</span>
<span class="lp-result-settled">settled</span>
</div>
<div class="lp-result-row lp-demo__row--fade">
<span class="lp-demo__player">Okafor</span>
<span class="lp-demo__num">$50</span>
<span class="lp-demo__num lp-demo__num--pos">+$30</span>
<span class="lp-result-due">due $30</span>
</div>
<div class="lp-result-row lp-demo__row--fade2">
<span class="lp-demo__player">Novak</span>
<span class="lp-demo__num">$100</span>
<span class="lp-demo__num lp-demo__num--neg">-$100</span>
<span class="lp-result-settled">settled</span>
</div>
</div>
</div>
</div>
</section>
{% if not is_logged_in %}
<section class="lp-bottom-cta">
<div class="lp-bottom-cta__inner">
<h2 class="lp-bottom-cta__title">Start tracking your league</h2>
<p class="lp-bottom-cta__sub">Free to use. No payment processor. Your data stays yours.</p>
<div class="lp-cta" style="justify-content:center;">
<a class="btn btn--primary btn--lg" href="{{ url_for('account.register') }}">Create an account →</a>
<a class="btn btn--ghost btn--lg" href="{{ url_for('public.explore') }}">Browse public leagues</a>
</div>
</div>
</section>
{% endif %}
{% endblock %} {% endblock %}
+292 -68
View File
@@ -1,102 +1,326 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block title %}{{ league.name }} · myboker.org{% endblock %} {% block title %}{{ league.name }} · myboker.org{% endblock %}
{% block subnav %}{% include "_league_subnav.html" %}{% endblock %} {% block sidebar %}{% include "_league_sidebar.html" %}{% endblock %}
{% block page_class %}page--app{% endblock %}
{% block content %} {% block content %}
{% if is_logged_in %} <div class="db-header">
<a class="league-back-link" href="{{ url_for('leagues.index') }}">← Leagues</a> <div class="db-header__info">
{% endif %} <div class="db-header__chips">
<div class="db-hero">
<div class="db-hero__bg-letter" aria-hidden="true">{{ league.name[0] | upper }}</div>
<div class="db-hero__body">
<div class="db-hero__chips">
{% if is_owner %}
<span class="db-chip db-chip--accent">Owner</span>
{% endif %}
<span class="db-chip">{{ league.visibility }}</span> <span class="db-chip">{{ league.visibility }}</span>
{% if is_owner %}<span class="db-chip db-chip--accent">Owner</span>{% endif %}
</div> </div>
<h1 class="hero__title" style="margin-bottom:8px;">{{ league.name }}</h1> <h1 class="db-header__title">{{ league.name }}</h1>
<p class="hero__sub" style="margin:0;">{{ league.description or 'No description set.' }}</p> {% if league.description %}<p class="db-header__sub">{{ league.description }}</p>{% endif %}
</div> </div>
{% if is_owner %}
<div class="db-header__actions">
<a class="btn btn--ghost btn--sm" href="{{ url_for('leagues.league_settings', league_ref=league.url_ref) }}">Settings</a>
<a class="btn btn--primary btn--sm" href="{{ url_for('leagues.sessions', league_ref=league.url_ref) }}">New session</a>
</div>
{% endif %}
</div> </div>
{% if counts.open_sessions > 0 %} {% if counts.open_sessions > 0 %}
<div class="db-live-bar"> <div class="db-live-alert">
<span class="live-badge">{{ counts.open_sessions }} live</span> <div class="db-live-alert__pulse"></div>
<span class="db-live-bar__text"> <span class="db-live-alert__text">
{% if counts.open_sessions == 1 %}A session is currently in progress.{% else %}{{ counts.open_sessions }} sessions are in progress.{% endif %} {{ counts.open_sessions }} session{{ 's' if counts.open_sessions != 1 else '' }} in progress
</span> </span>
<a class="btn btn--sm db-live-bar__btn" href="{{ url_for('leagues.sessions', league_ref=league.url_ref) }}">View sessions →</a> <a class="db-live-alert__link" href="{{ url_for('leagues.sessions', league_ref=league.url_ref) }}">View sessions →</a>
</div> </div>
{% endif %} {% endif %}
<div class="db-stats"> <div class="db-stat-grid">
<div class="stat stat--rail" style="--rail:var(--pos);"> <div class="db-stat">
<span class="kicker">Active players</span> <div class="db-stat__icon db-stat__icon--accent">
<div class="stat__val" style="color:var(--text-strong);">{{ counts.players }}</div> <svg width="18" height="18" viewBox="0 0 20 20" fill="none" aria-hidden="true">
<circle cx="8" cy="7" r="3" stroke="currentColor" stroke-width="1.5"/>
<path d="M2 17c0-3.314 2.686-6 6-6" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
<circle cx="14" cy="8" r="2.5" stroke="currentColor" stroke-width="1.5"/>
<path d="M18 17c0-2.761-1.79-5-4-5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
</svg>
</div>
<div>
<span class="db-stat__num">{{ counts.players }}</span>
<span class="db-stat__label">Players</span>
</div>
</div> </div>
<div class="stat stat--rail" style="--rail:var(--accent);"> <div class="db-stat">
<span class="kicker">Sessions</span> <div class="db-stat__icon db-stat__icon--pos">
<div class="stat__val" style="color:var(--text-strong);">{{ counts.sessions }}</div> <svg width="18" height="18" viewBox="0 0 20 20" fill="none" aria-hidden="true">
{% if counts.open_sessions > 0 %} <rect x="2.5" y="3.5" width="15" height="13" rx="2" stroke="currentColor" stroke-width="1.5"/>
<div class="stat__sub" style="color:var(--pos);">{{ counts.open_sessions }} open now</div> <path d="M6 2v3M14 2v3M2.5 8h15" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
{% endif %} </svg>
</div>
<div>
<span class="db-stat__num">{{ counts.sessions }}</span>
<span class="db-stat__label">Sessions{% if counts.open_sessions > 0 %} · <span style="color:var(--pos);">{{ counts.open_sessions }} live</span>{% endif %}</span>
</div>
</div> </div>
<div class="stat stat--rail" style="--rail:var(--warn);"> <div class="db-stat">
<span class="kicker">Ledger events</span> <div class="db-stat__icon db-stat__icon--warn">
<div class="stat__val" style="color:var(--text-strong);">{{ counts.events }}</div> <svg width="18" height="18" viewBox="0 0 20 20" fill="none" aria-hidden="true">
<path d="M5 3h10l3 3v11a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1Z" stroke="currentColor" stroke-width="1.5"/>
<path d="M7 8h6M7 11h4" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
</svg>
</div>
<div>
<span class="db-stat__num">{{ counts.events }}</span>
<span class="db-stat__label">Ledger events</span>
</div>
</div> </div>
<div class="stat stat--rail" style="--rail:var(--rolled);"> <div class="db-stat">
<span class="kicker">Seasons</span> <div class="db-stat__icon">
<div class="stat__val" style="color:var(--text-strong);">{{ counts.seasons }}</div> <svg width="18" height="18" viewBox="0 0 20 20" fill="none" aria-hidden="true">
<path d="M10 2v3M10 15v3M2 10h3M15 10h3" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
<circle cx="10" cy="10" r="4.5" stroke="currentColor" stroke-width="1.5"/>
</svg>
</div>
<div>
<span class="db-stat__num">{{ counts.seasons }}</span>
<span class="db-stat__label">Seasons</span>
</div>
</div> </div>
</div> </div>
<div class="dashboard-grid"> {% if cash_stats.has_data %}
<div class="db-cash">
<div class="db-cash__stat">
<span class="db-cash__label">Total cash in</span>
<span class="db-cash__val">{{ cash_stats.total_cash_in | money }}</span>
</div>
<div class="db-cash__div"></div>
<div class="db-cash__stat">
<span class="db-cash__label">Total paid out</span>
<span class="db-cash__val">{{ cash_stats.total_paid_out | money }}</span>
</div>
<div class="db-cash__div"></div>
<div class="db-cash__stat">
<span class="db-cash__label">Avg pot / session</span>
<span class="db-cash__val">{{ cash_stats.avg_pot | money }}</span>
</div>
<div class="db-cash__div"></div>
<div class="db-cash__stat">
<span class="db-cash__label">Avg players</span>
<span class="db-cash__val">{{ cash_stats.avg_players }}</span>
</div>
<div class="db-cash__div"></div>
<div class="db-cash__stat">
<span class="db-cash__label">Avg buy-in</span>
<span class="db-cash__val">{{ cash_stats.avg_buyin | money }}</span>
</div>
<div class="db-cash__div"></div>
<div class="db-cash__stat">
<span class="db-cash__label">Biggest session</span>
<span class="db-cash__val">{{ cash_stats.biggest_pot | money }}</span>
</div>
{% if cash_stats.avg_sessions_per_month > 0 %}
<div class="db-cash__div"></div>
<div class="db-cash__stat">
<span class="db-cash__label">Sessions / month</span>
<span class="db-cash__val">{{ cash_stats.avg_sessions_per_month }}</span>
</div>
{% endif %}
{% if cash_stats.total_fronts > 0 %}
<div class="db-cash__div"></div>
<div class="db-cash__stat">
<span class="db-cash__label">Fronts issued</span>
<span class="db-cash__val">{{ cash_stats.total_fronts | money }}</span>
</div>
{% endif %}
</div>
{% endif %}
<a class="dashboard-card panel" style="--card-accent:var(--warn);" href="{{ url_for('leagues.leaderboard', league_ref=league.url_ref) }}"> {% if cash_stats.has_sessions %}
<div class="dashboard-card__top"> <div class="db-cal"
<span class="kicker">Stats</span> id="db-cal"
<span class="dashboard-card__count">{{ counts.players }} ranked</span> data-sessions='{{ cash_stats.session_map | tojson }}'
data-first-year="{{ cash_stats.first_session_year }}">
<div class="db-cal__hdr">
<span class="db-cal__title">Session history</span>
<div class="db-cal__nav-group">
<button class="db-cal__nav" id="db-cal-prev" aria-label="Previous year">&#8249;</button>
<span class="db-cal__year" id="db-cal-year"></span>
<button class="db-cal__nav" id="db-cal-next" aria-label="Next year">&#8250;</button>
</div> </div>
<strong class="dashboard-card__title">Leaderboard</strong> </div>
<p class="dashboard-card__desc">All-time results, ROI, settlement totals, and standings.</p> <div class="db-cal__grid" id="db-cal-grid"></div>
<span class="dashboard-card__arrow"></span> <div class="db-cal__tip" id="db-cal-tip" hidden></div>
</div>
<script>
(function () {
var cal = document.getElementById('db-cal');
if (!cal) return;
var sessions = JSON.parse(cal.dataset.sessions);
var firstYear = parseInt(cal.dataset.firstYear) || new Date().getFullYear();
var MONTHS = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'];
var today = new Date(); today.setHours(0,0,0,0);
var todayStr = today.toISOString().slice(0,10);
var currentYear = today.getFullYear();
var year = currentYear;
var grid = document.getElementById('db-cal-grid');
var yearLbl = document.getElementById('db-cal-year');
var prevBtn = document.getElementById('db-cal-prev');
var nextBtn = document.getElementById('db-cal-next');
var tip = document.getElementById('db-cal-tip');
function ds(d) { return d.toISOString().slice(0,10); }
function buildCalendar(y) {
var start = new Date(y, 0, 1);
var dow = start.getDay();
start.setDate(start.getDate() - (dow === 0 ? 6 : dow - 1));
var end = new Date(y, 11, 31);
var edow = end.getDay();
if (edow !== 0) end.setDate(end.getDate() + (7 - edow));
var weeks = [], cur = new Date(start);
while (cur <= end) {
var days = [], monthLabel = '';
for (var i = 0; i < 7; i++) {
var d = new Date(cur.getFullYear(), cur.getMonth(), cur.getDate() + i);
var s = ds(d);
if (d.getDate() === 1 && !monthLabel) monthLabel = MONTHS[d.getMonth()];
days.push({ date: s, count: sessions[s] || 0, future: s > todayStr, out: d.getFullYear() !== y });
}
weeks.push({ days: days, month: monthLabel });
cur.setDate(cur.getDate() + 7);
}
return weeks;
}
function render(y) {
var weeks = buildCalendar(y);
grid.innerHTML = '';
weeks.forEach(function(week) {
var mc = document.createElement('div');
mc.className = 'db-cal__mc';
mc.textContent = week.month;
grid.appendChild(mc);
week.days.forEach(function(day) {
var cell = document.createElement('div');
var cls = 'db-cal__cell';
if (day.out) cls += ' db-cal__cell--out';
else if (day.future) cls += ' db-cal__cell--dim';
else if (day.count) cls += ' db-cal__cell--on' + (day.count > 1 ? ' db-cal__cell--multi' : '');
cell.className = cls;
cell.dataset.date = day.date;
cell.dataset.count = day.count;
grid.appendChild(cell);
});
});
yearLbl.textContent = y;
prevBtn.disabled = y <= firstYear;
nextBtn.disabled = y >= currentYear;
}
function showTip(cell, e) {
var d = cell.dataset.date;
if (!d) return;
var parts = d.split('-').map(Number);
var dateObj = new Date(parts[0], parts[1] - 1, parts[2]);
var label = dateObj.toLocaleDateString('en-US', { weekday:'short', month:'short', day:'numeric', year:'numeric' });
var count = parseInt(cell.dataset.count) || 0;
tip.textContent = count ? label + ' · ' + count + ' session' + (count > 1 ? 's' : '') : label;
tip.hidden = false;
moveTip(e);
}
function moveTip(e) {
var tw = tip.offsetWidth, th = tip.offsetHeight;
var x = e.clientX - tw / 2, y = e.clientY - th - 10;
x = Math.max(8, Math.min(x, window.innerWidth - tw - 8));
if (y < 8) y = e.clientY + 14;
tip.style.left = x + 'px';
tip.style.top = y + 'px';
}
grid.addEventListener('mouseover', function(e) {
var cell = e.target.closest('.db-cal__cell');
if (cell && !cell.classList.contains('db-cal__cell--out')) showTip(cell, e);
else tip.hidden = true;
});
grid.addEventListener('mousemove', function(e) { if (!tip.hidden) moveTip(e); });
grid.addEventListener('mouseleave', function() { tip.hidden = true; });
prevBtn.addEventListener('click', function() { if (year > firstYear) render(--year); });
nextBtn.addEventListener('click', function() { if (year < currentYear) render(++year); });
render(year);
})();
</script>
{% endif %}
<div class="db-nav-grid">
<a class="db-nav-tile db-nav-tile--sessions {% if counts.open_sessions > 0 %}db-nav-tile--live{% endif %}" href="{{ url_for('leagues.sessions', league_ref=league.url_ref) }}">
<div class="db-nav-tile__icon">
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" aria-hidden="true">
<rect x="3" y="4" width="18" height="16" rx="2.5" stroke="currentColor" stroke-width="1.6"/>
<path d="M3 9h18M8 3v2M16 3v2" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/>
<circle cx="8" cy="14" r="1.5" fill="currentColor"/>
<circle cx="12" cy="14" r="1.5" fill="currentColor"/>
<circle cx="16" cy="14" r="1.5" fill="currentColor"/>
</svg>
</div>
<div class="db-nav-tile__body">
<div class="db-nav-tile__title-row">
<strong class="db-nav-tile__title">Sessions</strong>
{% if counts.open_sessions > 0 %}
<span class="live-badge">{{ counts.open_sessions }} live</span>
{% endif %}
</div>
<p class="db-nav-tile__desc">Open a session to start tracking buy-ins and cashouts in real time. Close it when everyone is done.</p>
</div>
<span class="db-nav-tile__arrow"></span>
</a> </a>
<a class="dashboard-card panel" style="--card-accent:var(--pos);" href="{{ url_for('leagues.players', league_ref=league.url_ref) }}"> <a class="db-nav-tile db-nav-tile--leaderboard" href="{{ url_for('leagues.leaderboard', league_ref=league.url_ref) }}">
<div class="dashboard-card__top"> <div class="db-nav-tile__icon">
<span class="kicker">Roster</span> <svg width="22" height="22" viewBox="0 0 24 24" fill="none" aria-hidden="true">
<span class="dashboard-card__count">{{ counts.players }} active</span> <path d="M3 20h18M6 20V13M10 20V9M14 20V11M18 20V5" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/>
</svg>
</div> </div>
<strong class="dashboard-card__title">Players</strong> <div class="db-nav-tile__body">
<p class="dashboard-card__desc">Add and manage player records for stats and ledger entry.</p> <div class="db-nav-tile__title-row">
<span class="dashboard-card__arrow"></span> <strong class="db-nav-tile__title">Leaderboard</strong>
</div>
<p class="db-nav-tile__desc">Net profit, ROI, win rate, and rank delta. Filter by eligible players or switch to recent form.</p>
</div>
<span class="db-nav-tile__arrow"></span>
</a> </a>
<a class="dashboard-card panel {% if counts.open_sessions > 0 %}dashboard-card--live{% endif %}" style="--card-accent:var(--accent);" href="{{ url_for('leagues.sessions', league_ref=league.url_ref) }}"> <a class="db-nav-tile db-nav-tile--players" href="{{ url_for('leagues.players', league_ref=league.url_ref) }}">
<div class="dashboard-card__top"> <div class="db-nav-tile__icon">
<span class="kicker">Schedule</span> <svg width="22" height="22" viewBox="0 0 24 24" fill="none" aria-hidden="true">
{% if counts.open_sessions > 0 %} <circle cx="9" cy="8" r="3.5" stroke="currentColor" stroke-width="1.6"/>
<span class="live-badge">{{ counts.open_sessions }} live</span> <path d="M2.5 21c0-3.866 2.91-7 6.5-7" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/>
{% else %} <circle cx="16.5" cy="9.5" r="3" stroke="currentColor" stroke-width="1.6"/>
<span class="dashboard-card__count">{{ counts.sessions }} total</span> <path d="M21.5 21c0-3.314-2.239-6-5-6" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/>
{% endif %} </svg>
</div> </div>
<strong class="dashboard-card__title">Sessions</strong> <div class="db-nav-tile__body">
<p class="dashboard-card__desc">Create sessions, record events, and manage open/closed state.</p> <div class="db-nav-tile__title-row">
<span class="dashboard-card__arrow"></span> <strong class="db-nav-tile__title">Players</strong>
</div>
<p class="db-nav-tile__desc">View each player's full session history, net trend, and individual stats over time.</p>
</div>
<span class="db-nav-tile__arrow"></span>
</a> </a>
<a class="dashboard-card panel" style="--card-accent:var(--rolled);" href="{{ url_for('leagues.ledger', league_ref=league.url_ref) }}"> <a class="db-nav-tile db-nav-tile--ledger" href="{{ url_for('leagues.ledger', league_ref=league.url_ref) }}">
<div class="dashboard-card__top"> <div class="db-nav-tile__icon">
<span class="kicker">Accounting</span> <svg width="22" height="22" viewBox="0 0 24 24" fill="none" aria-hidden="true">
<span class="dashboard-card__count">{{ counts.events }} events</span> <path d="M5 3h14a1 1 0 0 1 1 1v14a1 1 0 0 1-1 1H9l-4-4V4a1 1 0 0 1 1-1Z" stroke="currentColor" stroke-width="1.6"/>
<path d="M9 19v-4H5M8 9h8M8 13h5" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/>
</svg>
</div> </div>
<strong class="dashboard-card__title">League ledger</strong> <div class="db-nav-tile__body">
<p class="dashboard-card__desc">Review all events, open debts, cash movement, and settlements.</p> <div class="db-nav-tile__title-row">
<span class="dashboard-card__arrow"></span> <strong class="db-nav-tile__title">League ledger</strong>
</div>
<p class="db-nav-tile__desc">Track what the house holds, what it owes, and outstanding fronts across all sessions.</p>
</div>
<span class="db-nav-tile__arrow"></span>
</a> </a>
</div> </div>
+5 -4
View File
@@ -1,12 +1,13 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block title %}Leaderboard · {{ league.name }} · myboker.org{% endblock %} {% block title %}Leaderboard · {{ league.name }} · myboker.org{% endblock %}
{% block subnav %}{% include "_league_subnav.html" %}{% endblock %} {% block sidebar %}{% include "_league_sidebar.html" %}{% endblock %}
{% block page_class %}page--app{% endblock %}
{% block content %} {% block content %}
<div class="hero" style="display:flex;flex-wrap:wrap;gap:var(--s6);align-items:flex-start;justify-content:space-between;"> <div class="page-header">
<div> <div>
<h1 class="hero__title">Leaderboard</h1> <h1 class="page-header__title">Leaderboard</h1>
<p class="hero__sub">{{ session_count }} session{{ 's' if session_count != 1 else '' }}{% if selected_session_date %} through {{ selected_session_label }}{% endif %}</p> <p class="page-header__sub">{{ session_count }} session{{ 's' if session_count != 1 else '' }}{% if selected_session_date %} through {{ selected_session_label }}{% endif %}</p>
</div> </div>
<div class="leaderboard-hero-stats"> <div class="leaderboard-hero-stats">
<div class="stat"> <div class="stat">
+2 -1
View File
@@ -1,6 +1,7 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block title %}Ledger · {{ league.name }} · myboker.org{% endblock %} {% block title %}Ledger · {{ league.name }} · myboker.org{% endblock %}
{% block subnav %}{% include "_league_subnav.html" %}{% endblock %} {% block sidebar %}{% include "_league_sidebar.html" %}{% endblock %}
{% block page_class %}page--app{% endblock %}
{% block content %} {% block content %}
<div class="cash-strip"> <div class="cash-strip">
+2 -1
View File
@@ -1,6 +1,7 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block title %}{{ player.player_name }} · {{ league.name }} · myboker.org{% endblock %} {% block title %}{{ player.player_name }} · {{ league.name }} · myboker.org{% endblock %}
{% block subnav %}{% include "_league_subnav.html" %}{% endblock %} {% block sidebar %}{% include "_league_sidebar.html" %}{% endblock %}
{% block page_class %}page--app{% endblock %}
{% block content %} {% block content %}
+9 -1
View File
@@ -1,8 +1,16 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block title %}Players · {{ league.name }} · myboker.org{% endblock %} {% block title %}Players · {{ league.name }} · myboker.org{% endblock %}
{% block subnav %}{% include "_league_subnav.html" %}{% endblock %} {% block sidebar %}{% include "_league_sidebar.html" %}{% endblock %}
{% block page_class %}page--app{% endblock %}
{% block content %} {% block content %}
<div class="page-header">
<div>
<h1 class="page-header__title">Players</h1>
<p class="page-header__sub">Roster for {{ league.name }}</p>
</div>
</div>
{% if can_manage %} {% if can_manage %}
<form class="panel form-card" method="post" style="margin-bottom:20px;"> <form class="panel form-card" method="post" style="margin-bottom:20px;">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"> <input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
+2 -1
View File
@@ -1,6 +1,7 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block title %}{{ session_model.display_label }} · {{ league.name }} · myboker.org{% endblock %} {% block title %}{{ session_model.display_label }} · {{ league.name }} · myboker.org{% endblock %}
{% block subnav %}{% include "_league_subnav.html" %}{% endblock %} {% block sidebar %}{% include "_league_sidebar.html" %}{% endblock %}
{% block page_class %}page--app{% endblock %}
{% block content %} {% block content %}
<div class="hero" style="padding-top:0;padding-bottom:12px;display:flex;flex-wrap:wrap;gap:12px;align-items:flex-end;justify-content:space-between;"> <div class="hero" style="padding-top:0;padding-bottom:12px;display:flex;flex-wrap:wrap;gap:12px;align-items:flex-end;justify-content:space-between;">
+2 -2
View File
@@ -1,7 +1,7 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block title %}{{ session_model.display_label }} · {{ league.name }} · myboker.org{% endblock %} {% block title %}{{ session_model.display_label }} · {{ league.name }} · myboker.org{% endblock %}
{% block subnav %}{% include "_league_subnav.html" %}{% endblock %} {% block sidebar %}{% include "_league_sidebar.html" %}{% endblock %}
{% block page_class %}page--session{% endblock %} {% block page_class %}page--app{% endblock %}
{% block content %} {% block content %}
<div class="breadcrumb"> <div class="breadcrumb">
+9 -1
View File
@@ -1,8 +1,16 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block title %}Sessions · {{ league.name }} · myboker.org{% endblock %} {% block title %}Sessions · {{ league.name }} · myboker.org{% endblock %}
{% block subnav %}{% include "_league_subnav.html" %}{% endblock %} {% block sidebar %}{% include "_league_sidebar.html" %}{% endblock %}
{% block page_class %}page--app{% endblock %}
{% block content %} {% block content %}
<div class="page-header">
<div>
<h1 class="page-header__title">Sessions</h1>
<p class="page-header__sub">All game sessions for {{ league.name }}</p>
</div>
</div>
<div class="stack"> <div class="stack">
{% if can_manage %} {% if can_manage %}
+2 -1
View File
@@ -1,6 +1,7 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block title %}Settings · {{ league.name }} · myboker.org{% endblock %} {% block title %}Settings · {{ league.name }} · myboker.org{% endblock %}
{% block subnav %}{% include "_league_subnav.html" %}{% endblock %} {% block sidebar %}{% include "_league_sidebar.html" %}{% endblock %}
{% block page_class %}page--app{% endblock %}
{% block content %} {% block content %}
+100 -105
View File
@@ -2,133 +2,128 @@
{% block title %}Leagues · myboker.org{% endblock %} {% block title %}Leagues · myboker.org{% endblock %}
{% block content %} {% block content %}
<div class="hero" style="padding-bottom:12px;"> <div class="page-header">
<p class="hero__kicker">Leagues</p> <div>
<h1 class="hero__title">Your poker leagues</h1> <h1 class="page-header__title">Your leagues</h1>
<p class="hero__sub">Owner and manager workspace.</p> {% if leagues %}
</div> <p class="page-header__sub">
{{ page_summary.league_count }} league{{ 's' if page_summary.league_count != 1 else '' }}
<div class="toolbar"> {% if page_summary.live_count > 0 %}&nbsp;·&nbsp;<span style="color:var(--pos);">{{ page_summary.live_count }} live</span>{% endif %}
<div class="toolbar__left"> &nbsp;·&nbsp;{{ page_summary.sessions_count }} session{{ 's' if page_summary.sessions_count != 1 else '' }}
<a class="btn btn--primary" href="{{ url_for('leagues.new') }}">New league</a> </p>
{% endif %}
</div>
<div class="page-header__actions">
<a class="btn btn--primary btn--sm" href="{{ url_for('leagues.new') }}">New league</a>
</div> </div>
</div> </div>
{% if leagues %} {% if leagues %}
<div class="league-hub-summary">
<div class="stat stat--rail" style="--rail:var(--accent);">
<span class="kicker">Leagues</span>
<div class="stat__val" style="color:var(--num)">{{ page_summary.league_count }}</div>
</div>
<div class="stat stat--rail" style="--rail:var(--pos);">
<span class="kicker">Live</span>
<div class="stat__val" style="color:var(--num)">{{ page_summary.live_count }}</div>
</div>
<div class="stat stat--rail" style="--rail:var(--rolled);">
<span class="kicker">Sessions</span>
<div class="stat__val" style="color:var(--num)">{{ page_summary.sessions_count }}</div>
</div>
<div class="stat stat--rail" style="--rail:var(--warn);">
<span class="kicker">Open items</span>
<div class="stat__val {{ 'num-neg' if page_summary.open_items_cents > 0 else 'num-muted' }}">{{ page_summary.open_items_cents | money }}</div>
</div>
</div>
<div class="league-board"> <div class="li-board">
{% for item in leagues %} {% for item in leagues %}
{% set league = item.league %} {% set league = item.league %}
{% set membership = item.membership %} {% set membership = item.membership %}
<article class="panel league-row"> <article class="li-card{% if item.live_session %} li-card--live{% endif %}">
<div class="league-row__identity">
<a class="league-row__title" href="{{ url_for('leagues.dashboard', league_ref=league.url_ref) }}"> <div class="li-card__aside">
<div> <span class="li-card__initial">{{ league.name[0]|upper }}</span>
<span class="kicker">{{ membership.role }}</span> </div>
<h2>{{ league.name }}</h2>
</div> <div class="li-card__body">
{% if item.live_session %} <div class="li-card__head">
<span class="live-badge">{{ session_label(item.live_session) }}</span> <div class="li-card__info">
{% else %} <div class="li-card__name-row">
<span class="status-pill status-pill--closed">idle</span> <a class="li-card__name" href="{{ url_for('leagues.dashboard', league_ref=league.url_ref) }}">{{ league.name }}</a>
{% endif %} {% if item.live_session %}
</a> <span class="live-badge">{{ session_label(item.live_session) }}</span>
<div class="league-row__chips"> {% else %}
<span class="db-chip {% if league.visibility == 'public' %}db-chip--public{% endif %}"> <span class="status-pill status-pill--closed">idle</span>
{% if league.visibility == 'public' %} {% endif %}
<svg width="11" height="11" viewBox="0 0 20 20" fill="none" aria-hidden="true"> </div>
<circle cx="10" cy="10" r="8" stroke="currentColor" stroke-width="1.6"/> <div class="li-card__chips">
<path d="M10 2c-2.5 3-2.5 13 0 16" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" fill="none"/> <span class="db-chip db-chip--accent">{{ membership.role }}</span>
<path d="M10 2c2.5 3 2.5 13 0 16" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" fill="none"/> <span class="db-chip {% if league.visibility == 'public' %}db-chip--public{% endif %}">
<path d="M2.5 10h15" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"/> {% if league.visibility == 'public' %}
</svg> <svg width="10" height="10" viewBox="0 0 20 20" fill="none" aria-hidden="true">
public <circle cx="10" cy="10" r="8" stroke="currentColor" stroke-width="1.6"/>
{% else %} <path d="M10 2c-2.5 3-2.5 13 0 16" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" fill="none"/>
<svg width="10" height="12" viewBox="0 0 10 12" fill="none" aria-hidden="true"> <path d="M10 2c2.5 3 2.5 13 0 16" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" fill="none"/>
<rect x="1" y="5" width="8" height="6.5" rx="1.5" stroke="currentColor" stroke-width="1.5"/> <path d="M2.5 10h15" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"/>
<path d="M3 5V3.5A2 2 0 0 1 7 3.5V5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/> </svg>
</svg> public
private {% 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 %} {% endif %}
</span> </div>
</div>
<p class="league-row__desc">
{% if league.description %}{{ league.description }}{% else %}{{ league.slug }}{% endif %}
</p>
<div class="league-row__meta">
{% if item.latest_session %}
<span>Latest {{ session_label(item.latest_session) }}</span>
{% else %}
<span>No sessions yet</span>
{% endif %}
{% if item.leader %}
<span>Leader {{ item.leader.player_name }} · {{ item.leader.total_net_cents | money }}</span>
{% endif %}
</div>
</div>
<div class="league-row__metrics"> <div class="li-card__stats">
<div><span>Players</span><strong>{{ item.players_count }}</strong></div> <div class="li-stat">
<div><span>Sessions</span><strong>{{ item.sessions_count }}</strong></div> <span>Players</span>
<div><span>Paid out</span><strong>{{ item.cash_paid_out_cents | money }}</strong></div> <strong>{{ item.players_count }}</strong>
<div><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> <div class="li-stat">
<span>Sessions</span>
<div class="league-row__leaders"> <strong>{{ item.sessions_count }}</strong>
<div class="league-row__section-head"> </div>
<span class="kicker">Top 3</span> <div class="li-stat">
<a href="{{ url_for('leagues.leaderboard', league_ref=league.url_ref) }}">Full board</a> <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> </div>
{% if item.top_players %} {% if item.top_players %}
<ol class="league-top-list"> <div class="li-card__leaders">
{% for player in item.top_players %} <span class="li-card__leaders-label">Leaders</span>
<li> <div class="li-podium">
<span>{{ player.player_name }}</span> {% for player in item.top_players %}
<strong class="{{ 'num-pos' if player.total_net_cents > 0 else 'num-neg' if player.total_net_cents < 0 else 'num-muted' }}"> <div class="li-podium-item li-podium-item--{{ loop.index }}">
{{ '+' if player.total_net_cents > 0 else '' }}{{ player.total_net_cents | money }} <span class="li-podium-rank">#{{ loop.index }}</span>
</strong> <span class="li-podium-name">{{ player.player_name }}</span>
</li> <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' }}">
{% endfor %} {{ '+' if player.total_net_cents > 0 else '' }}{{ player.total_net_cents | money }}
</ol> </strong>
{% else %} </div>
<div class="league-row__empty"> {% endfor %}
<span>No results yet</span> </div>
<a href="{{ url_for('leagues.sessions', league_ref=league.url_ref) }}">Create a session</a>
</div> </div>
{% endif %} {% 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>
<a class="li-action-link" href="{{ url_for('leagues.ledger', league_ref=league.url_ref) }}">Ledger</a>
</div>
</div>
</div> </div>
<div class="league-row__actions">
<a class="btn btn--primary btn--sm" href="{{ url_for('leagues.dashboard', league_ref=league.url_ref) }}">Open</a>
<a class="btn btn--ghost btn--sm" href="{{ url_for('leagues.sessions', league_ref=league.url_ref) }}">Sessions</a>
<a class="btn btn--ghost btn--sm" href="{{ url_for('leagues.ledger', league_ref=league.url_ref) }}">Ledger</a>
<a class="btn btn--ghost btn--sm" href="{{ url_for('leagues.players', league_ref=league.url_ref) }}">Players</a>
</div>
</article> </article>
{% endfor %} {% endfor %}
</div> </div>
{% else %} {% else %}
<div class="panel empty-panel"> <div class="panel empty-panel">
<h2 class="panel__title">No leagues yet</h2> <h2 class="panel__title">No leagues yet</h2>
<p class="muted-text">Create the first league to start adding players, sessions, and ledger events.</p> <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> <a class="btn btn--primary" href="{{ url_for('leagues.new') }}">Create league</a>
</div> </div>
{% endif %} {% endif %}