require search first in explore
This commit is contained in:
5 files changed
+194
-47
No files matched your search
+6
-2
@@ -10,7 +10,7 @@ DEFAULT_DATABASE_URL = f"sqlite:///{BASE_DIR / 'data' / 'boker-dev.sqlite3'}"
|
|||||||
DEFAULT_SECRET_KEY = "change-this-before-deploying"
|
DEFAULT_SECRET_KEY = "change-this-before-deploying"
|
||||||
|
|
||||||
ELIGIBLE_MIN_SESSIONS = 3
|
ELIGIBLE_MIN_SESSIONS = 3
|
||||||
APP_VERSION = "2.5.30"
|
APP_VERSION = "2.5.31"
|
||||||
|
|
||||||
|
|
||||||
def load_local_env(env_path: Path) -> None:
|
def load_local_env(env_path: Path) -> None:
|
||||||
@@ -44,7 +44,11 @@ class Config:
|
|||||||
APP_BASE_URL: str = os.getenv("APP_BASE_URL", "http://localhost:5000")
|
APP_BASE_URL: str = os.getenv("APP_BASE_URL", "http://localhost:5000")
|
||||||
MAIL_SERVER: str = os.getenv("MAIL_SERVER", "")
|
MAIL_SERVER: str = os.getenv("MAIL_SERVER", "")
|
||||||
MAIL_PORT: int = int(os.getenv("MAIL_PORT", "587"))
|
MAIL_PORT: int = int(os.getenv("MAIL_PORT", "587"))
|
||||||
MAIL_USE_TLS: bool = os.getenv("MAIL_USE_TLS", "true").lower() in ("true", "1", "yes")
|
MAIL_USE_TLS: bool = os.getenv("MAIL_USE_TLS", "true").lower() in (
|
||||||
|
"true",
|
||||||
|
"1",
|
||||||
|
"yes",
|
||||||
|
)
|
||||||
MAIL_USE_SSL: bool = os.getenv(
|
MAIL_USE_SSL: bool = os.getenv(
|
||||||
"MAIL_USE_SSL",
|
"MAIL_USE_SSL",
|
||||||
"true" if MAIL_PORT == 465 else "false",
|
"true" if MAIL_PORT == 465 else "false",
|
||||||
|
|||||||
+11
-15
@@ -67,19 +67,6 @@ def sitemap_xml():
|
|||||||
_sitemap_url(url_for("public.terms"), "0.3", "yearly"),
|
_sitemap_url(url_for("public.terms"), "0.3", "yearly"),
|
||||||
]
|
]
|
||||||
|
|
||||||
from boker.db import database_extensions_available
|
|
||||||
|
|
||||||
if database_extensions_available():
|
|
||||||
try:
|
|
||||||
from boker.league_repositories import list_public_leagues
|
|
||||||
|
|
||||||
for league in list_public_leagues():
|
|
||||||
urls.append(_sitemap_url(url_for("leagues.dashboard", league_ref=league.url_ref), "0.6", "weekly"))
|
|
||||||
urls.append(_sitemap_url(url_for("leagues.leaderboard", league_ref=league.url_ref), "0.5", "weekly"))
|
|
||||||
urls.append(_sitemap_url(url_for("leagues.sessions", league_ref=league.url_ref), "0.5", "weekly"))
|
|
||||||
except SQLAlchemyError:
|
|
||||||
current_app.logger.warning("Sitemap public league query failed", exc_info=True)
|
|
||||||
|
|
||||||
body = (
|
body = (
|
||||||
'<?xml version="1.0" encoding="UTF-8"?>\n'
|
'<?xml version="1.0" encoding="UTF-8"?>\n'
|
||||||
'<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n'
|
'<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n'
|
||||||
@@ -111,9 +98,18 @@ def explore():
|
|||||||
from boker.league_repositories import league_counts, list_public_leagues
|
from boker.league_repositories import league_counts, list_public_leagues
|
||||||
|
|
||||||
q = request.args.get("q", "").strip()
|
q = request.args.get("q", "").strip()
|
||||||
leagues = list_public_leagues(q) if database_extensions_available() else []
|
leagues = []
|
||||||
|
counts = {}
|
||||||
|
if q and database_extensions_available():
|
||||||
|
try:
|
||||||
|
leagues = list_public_leagues(q)
|
||||||
counts = {league.id: league_counts(league.id) for league in leagues}
|
counts = {league.id: league_counts(league.id) for league in leagues}
|
||||||
return render_template("explore.html", leagues=leagues, counts=counts, q=q)
|
except SQLAlchemyError as exc:
|
||||||
|
from boker.db import db
|
||||||
|
|
||||||
|
db.session.rollback()
|
||||||
|
current_app.logger.warning("Explore public league search failed: %s", exc.__class__.__name__)
|
||||||
|
return render_template("explore.html", leagues=leagues, counts=counts, q=q, has_searched=bool(q))
|
||||||
|
|
||||||
|
|
||||||
@public_bp.get("/leaderboard")
|
@public_bp.get("/leaderboard")
|
||||||
|
|||||||
+95
-12
@@ -5367,12 +5367,28 @@ select.control { cursor: pointer; }
|
|||||||
}
|
}
|
||||||
.settings-profile__meta { display: flex; flex-direction: column; }
|
.settings-profile__meta { display: flex; flex-direction: column; }
|
||||||
|
|
||||||
|
.sr-only {
|
||||||
|
position: absolute;
|
||||||
|
width: 1px;
|
||||||
|
height: 1px;
|
||||||
|
padding: 0;
|
||||||
|
margin: -1px;
|
||||||
|
overflow: hidden;
|
||||||
|
clip: rect(0, 0, 0, 0);
|
||||||
|
white-space: nowrap;
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
|
||||||
/* ================================================================
|
/* ================================================================
|
||||||
EXPLORE PAGE REDESIGN
|
EXPLORE PAGE REDESIGN
|
||||||
================================================================ */
|
================================================================ */
|
||||||
.explore-header {
|
.explore-hero {
|
||||||
|
max-width: 720px;
|
||||||
margin-bottom: 28px;
|
margin-bottom: 28px;
|
||||||
}
|
}
|
||||||
|
.explore-header {
|
||||||
|
margin-bottom: 18px;
|
||||||
|
}
|
||||||
.explore-header__eyebrow {
|
.explore-header__eyebrow {
|
||||||
font: 600 11px var(--font-mono);
|
font: 600 11px var(--font-mono);
|
||||||
letter-spacing: .1em;
|
letter-spacing: .1em;
|
||||||
@@ -5381,30 +5397,38 @@ select.control { cursor: pointer; }
|
|||||||
margin: 0 0 10px;
|
margin: 0 0 10px;
|
||||||
}
|
}
|
||||||
.explore-header__title {
|
.explore-header__title {
|
||||||
font-size: clamp(22px, 3vw, 30px);
|
font-size: clamp(26px, 4vw, 42px);
|
||||||
font-weight: 800;
|
font-weight: 800;
|
||||||
color: var(--text-strong);
|
color: var(--text-strong);
|
||||||
letter-spacing: -.025em;
|
margin: 0 0 8px;
|
||||||
margin: 0 0 6px;
|
|
||||||
}
|
}
|
||||||
.explore-header__sub {
|
.explore-header__sub {
|
||||||
font-size: .875rem;
|
max-width: 620px;
|
||||||
|
font-size: .9375rem;
|
||||||
|
line-height: 1.6;
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.explore-search-row {
|
.explore-search-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 8px;
|
align-items: center;
|
||||||
margin-bottom: 28px;
|
gap: 10px;
|
||||||
max-width: 560px;
|
max-width: 640px;
|
||||||
|
padding: 8px;
|
||||||
|
background: var(--surface);
|
||||||
|
border: 1px solid var(--border-strong);
|
||||||
|
border-radius: var(--r-lg);
|
||||||
|
box-shadow: var(--shadow-sm);
|
||||||
}
|
}
|
||||||
.explore-search-row .field-input {
|
.explore-search-row .field-input {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
background: var(--surface);
|
min-width: 0;
|
||||||
border: 1px solid var(--border-strong);
|
height: 42px;
|
||||||
border-radius: var(--r-md);
|
background: var(--surface-sunk);
|
||||||
padding: 10px 14px;
|
border: 1px solid var(--border);
|
||||||
|
border-radius: var(--r-sm);
|
||||||
|
padding: 0 14px;
|
||||||
font-size: .9375rem;
|
font-size: .9375rem;
|
||||||
color: var(--text);
|
color: var(--text);
|
||||||
transition: border-color .15s, box-shadow .15s;
|
transition: border-color .15s, box-shadow .15s;
|
||||||
@@ -5415,6 +5439,30 @@ select.control { cursor: pointer; }
|
|||||||
box-shadow: 0 0 0 3px var(--accent-a22);
|
box-shadow: 0 0 0 3px var(--accent-a22);
|
||||||
}
|
}
|
||||||
.explore-search-row .field-input::placeholder { color: var(--text-muted); }
|
.explore-search-row .field-input::placeholder { color: var(--text-muted); }
|
||||||
|
.explore-privacy-note {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
margin-top: 12px;
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: .8125rem;
|
||||||
|
}
|
||||||
|
.explore-privacy-note__dot {
|
||||||
|
width: 6px;
|
||||||
|
height: 6px;
|
||||||
|
border-radius: 1px;
|
||||||
|
background: var(--accent);
|
||||||
|
opacity: .82;
|
||||||
|
transform: rotate(45deg);
|
||||||
|
}
|
||||||
|
.explore-results-head {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin: 0 0 14px;
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-size: .8125rem;
|
||||||
|
}
|
||||||
|
|
||||||
.explore-cards {
|
.explore-cards {
|
||||||
display: grid;
|
display: grid;
|
||||||
@@ -5486,6 +5534,41 @@ select.control { cursor: pointer; }
|
|||||||
color: var(--accent);
|
color: var(--accent);
|
||||||
transition: gap .15s;
|
transition: gap .15s;
|
||||||
}
|
}
|
||||||
|
.explore-empty {
|
||||||
|
max-width: 620px;
|
||||||
|
display: grid;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 22px;
|
||||||
|
border: 1px dashed var(--border-strong);
|
||||||
|
border-radius: var(--r-lg);
|
||||||
|
background: var(--surface-sunk);
|
||||||
|
}
|
||||||
|
.explore-empty h2 {
|
||||||
|
margin: 0;
|
||||||
|
color: var(--text-strong);
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
.explore-empty p {
|
||||||
|
margin: 0;
|
||||||
|
color: var(--muted);
|
||||||
|
line-height: 1.55;
|
||||||
|
font-size: .875rem;
|
||||||
|
}
|
||||||
|
.explore-empty .btn {
|
||||||
|
justify-self: start;
|
||||||
|
margin-top: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 580px) {
|
||||||
|
.explore-search-row {
|
||||||
|
align-items: stretch;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
.explore-search-row .field-input,
|
||||||
|
.explore-search-row .btn {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* ================================================================
|
/* ================================================================
|
||||||
LANDING — stats bar
|
LANDING — stats bar
|
||||||
|
|||||||
+29
-18
@@ -1,21 +1,32 @@
|
|||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
{% block title %}Explore Public Home Poker Leagues · myboker.org{% endblock %}
|
{% block title %}Search Public Home Poker Leagues · myboker.org{% endblock %}
|
||||||
{% block meta_description %}Browse public home poker league results, leaderboards, sessions, and player stats from groups using myboker.org.{% endblock %}
|
{% block meta_description %}Search public home poker league results, leaderboards, sessions, and player stats from groups using myboker.org.{% endblock %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
|
||||||
<div class="explore-header">
|
<section class="explore-hero">
|
||||||
|
<div class="explore-header">
|
||||||
<p class="explore-header__eyebrow">Public leagues</p>
|
<p class="explore-header__eyebrow">Public leagues</p>
|
||||||
<h1 class="explore-header__title">Explore</h1>
|
<h1 class="explore-header__title">Search public leagues</h1>
|
||||||
<p class="explore-header__sub">Browse poker leagues that have made their results publicly visible.</p>
|
<p class="explore-header__sub">Public league pages are discoverable by name, but myboker does not list every public league by default.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form method="get" action="{{ url_for('public.explore') }}" class="explore-search-row">
|
<form method="get" action="{{ url_for('public.explore') }}" class="explore-search-row">
|
||||||
<input class="field-input" type="text" name="q" value="{{ q }}" placeholder="Search by league name…" autocomplete="off">
|
<label class="sr-only" for="league-search">Search by league name</label>
|
||||||
|
<input class="field-input" id="league-search" type="search" 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 %}
|
<div class="explore-privacy-note">
|
||||||
|
<span class="explore-privacy-note__dot"></span>
|
||||||
|
<span>Leagues are private unless an owner explicitly makes results public.</span>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{% if has_searched and leagues %}
|
||||||
|
<div class="explore-results-head">
|
||||||
|
<span>{{ leagues|length }} result{{ 's' if leagues|length != 1 else '' }} for "{{ q }}"</span>
|
||||||
|
</div>
|
||||||
<div class="explore-cards">
|
<div class="explore-cards">
|
||||||
{% for league in leagues %}
|
{% for league in leagues %}
|
||||||
{% set c = counts.get(league.id, {}) %}
|
{% set c = counts.get(league.id, {}) %}
|
||||||
@@ -49,16 +60,16 @@
|
|||||||
</a>
|
</a>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
{% elif q %}
|
{% elif has_searched %}
|
||||||
<div class="panel empty-panel" style="max-width:480px;">
|
<div class="explore-empty">
|
||||||
<h3 class="panel__title">No results for "{{ q }}"</h3>
|
<h2>No public leagues found</h2>
|
||||||
<p class="muted-text">Try a shorter search or browse all leagues.</p>
|
<p>No public league matched "{{ q }}". Check the spelling or ask the league owner for their shared link.</p>
|
||||||
<a class="btn btn--ghost btn--sm" href="{{ url_for('public.explore') }}">Browse all</a>
|
<a class="btn btn--ghost btn--sm" href="{{ url_for('public.explore') }}">Reset search</a>
|
||||||
</div>
|
</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class="panel empty-panel" style="max-width:480px;">
|
<div class="explore-empty explore-empty--start">
|
||||||
<h3 class="panel__title">No public leagues yet</h3>
|
<h2>Enter a league name to search</h2>
|
||||||
<p class="muted-text">League owners can set their league to public in league settings.</p>
|
<p>This page does not expose a directory of every public league. Search only returns leagues whose owners have chosen public visibility.</p>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import unittest
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from app import create_app
|
from app import create_app
|
||||||
|
from boker.auth import hash_password
|
||||||
from boker.db import db
|
from boker.db import db
|
||||||
|
|
||||||
|
|
||||||
@@ -60,6 +61,58 @@ class SeoTests(unittest.TestCase):
|
|||||||
self.assertIn("<loc>https://myboker.org/explore</loc>", body)
|
self.assertIn("<loc>https://myboker.org/explore</loc>", body)
|
||||||
self.assertIn("<loc>https://myboker.org/help</loc>", body)
|
self.assertIn("<loc>https://myboker.org/help</loc>", body)
|
||||||
|
|
||||||
|
def test_explore_requires_search_before_showing_public_leagues(self):
|
||||||
|
with self.app.app_context():
|
||||||
|
from boker.db_models import League, User
|
||||||
|
|
||||||
|
owner = User(email="owner@example.com", password_hash=hash_password("password123"))
|
||||||
|
db.session.add(owner)
|
||||||
|
db.session.flush()
|
||||||
|
league = League(
|
||||||
|
name="Friday Night Poker",
|
||||||
|
slug="friday-night-poker",
|
||||||
|
public_key="friday1",
|
||||||
|
created_by_user_id=owner.id,
|
||||||
|
visibility="public",
|
||||||
|
)
|
||||||
|
db.session.add(league)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
default_response = self.client.get("/explore")
|
||||||
|
default_html = default_response.get_data(as_text=True)
|
||||||
|
search_response = self.client.get("/explore?q=Friday")
|
||||||
|
search_html = search_response.get_data(as_text=True)
|
||||||
|
sitemap_response = self.client.get("/sitemap.xml")
|
||||||
|
sitemap_xml = sitemap_response.get_data(as_text=True)
|
||||||
|
|
||||||
|
self.assertEqual(default_response.status_code, 200)
|
||||||
|
self.assertIn("Enter a league name to search", default_html)
|
||||||
|
self.assertNotIn("Friday Night Poker", default_html)
|
||||||
|
self.assertEqual(search_response.status_code, 200)
|
||||||
|
self.assertIn("Friday Night Poker", search_html)
|
||||||
|
self.assertNotIn("/l/friday-night-poker-friday1", sitemap_xml)
|
||||||
|
|
||||||
|
def test_explore_search_handles_missing_database_tables(self):
|
||||||
|
with tempfile.TemporaryDirectory() as tmpdir:
|
||||||
|
db_path = Path(tmpdir) / "missing-tables.sqlite3"
|
||||||
|
app = create_app(
|
||||||
|
{
|
||||||
|
"TESTING": True,
|
||||||
|
"SQLALCHEMY_DATABASE_URI": f"sqlite:///{db_path}",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
client = app.test_client()
|
||||||
|
|
||||||
|
response = client.get("/explore?q=Friday")
|
||||||
|
|
||||||
|
with app.app_context():
|
||||||
|
db.session.remove()
|
||||||
|
db.engine.dispose()
|
||||||
|
|
||||||
|
html = response.get_data(as_text=True)
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertIn("No public leagues found", html)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
unittest.main()
|
unittest.main()
|
||||||
Reference in new issue
Block a user