From 737d6d7561f0d25aa1b27f63ef2fad92a535f8db Mon Sep 17 00:00:00 2001 From: SowinskiBraeden Date: Mon, 29 Jun 2026 12:42:50 -0700 Subject: [PATCH] require search first in explore --- boker/config.py | 8 ++- boker/routes/public.py | 28 +++++------ static/css/style.css | 107 ++++++++++++++++++++++++++++++++++++----- templates/explore.html | 55 ++++++++++++--------- tests/test_seo.py | 53 ++++++++++++++++++++ 5 files changed, 199 insertions(+), 52 deletions(-) diff --git a/boker/config.py b/boker/config.py index cb29732..5a95074 100644 --- a/boker/config.py +++ b/boker/config.py @@ -10,7 +10,7 @@ DEFAULT_DATABASE_URL = f"sqlite:///{BASE_DIR / 'data' / 'boker-dev.sqlite3'}" DEFAULT_SECRET_KEY = "change-this-before-deploying" ELIGIBLE_MIN_SESSIONS = 3 -APP_VERSION = "2.5.30" +APP_VERSION = "2.5.31" 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") MAIL_SERVER: str = os.getenv("MAIL_SERVER", "") 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", "true" if MAIL_PORT == 465 else "false", diff --git a/boker/routes/public.py b/boker/routes/public.py index 0cf144f..137be40 100644 --- a/boker/routes/public.py +++ b/boker/routes/public.py @@ -67,19 +67,6 @@ def sitemap_xml(): _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 = ( '\n' '\n' @@ -111,9 +98,18 @@ def explore(): from boker.league_repositories import league_counts, list_public_leagues q = request.args.get("q", "").strip() - leagues = list_public_leagues(q) if database_extensions_available() else [] - counts = {league.id: league_counts(league.id) for league in leagues} - return render_template("explore.html", leagues=leagues, counts=counts, q=q) + 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} + 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") diff --git a/static/css/style.css b/static/css/style.css index 09da5f2..e4cbec0 100644 --- a/static/css/style.css +++ b/static/css/style.css @@ -5367,12 +5367,28 @@ select.control { cursor: pointer; } } .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-header { +.explore-hero { + max-width: 720px; margin-bottom: 28px; } +.explore-header { + margin-bottom: 18px; +} .explore-header__eyebrow { font: 600 11px var(--font-mono); letter-spacing: .1em; @@ -5381,30 +5397,38 @@ select.control { cursor: pointer; } margin: 0 0 10px; } .explore-header__title { - font-size: clamp(22px, 3vw, 30px); + font-size: clamp(26px, 4vw, 42px); font-weight: 800; color: var(--text-strong); - letter-spacing: -.025em; - margin: 0 0 6px; + margin: 0 0 8px; } .explore-header__sub { - font-size: .875rem; + max-width: 620px; + font-size: .9375rem; + line-height: 1.6; color: var(--text-muted); margin: 0; } .explore-search-row { display: flex; - gap: 8px; - margin-bottom: 28px; - max-width: 560px; + align-items: center; + gap: 10px; + 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 { flex: 1; - background: var(--surface); - border: 1px solid var(--border-strong); - border-radius: var(--r-md); - padding: 10px 14px; + min-width: 0; + height: 42px; + background: var(--surface-sunk); + border: 1px solid var(--border); + border-radius: var(--r-sm); + padding: 0 14px; font-size: .9375rem; color: var(--text); transition: border-color .15s, box-shadow .15s; @@ -5415,6 +5439,30 @@ select.control { cursor: pointer; } box-shadow: 0 0 0 3px var(--accent-a22); } .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 { display: grid; @@ -5486,6 +5534,41 @@ select.control { cursor: pointer; } color: var(--accent); 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 diff --git a/templates/explore.html b/templates/explore.html index 5ba57c2..8b60f51 100644 --- a/templates/explore.html +++ b/templates/explore.html @@ -1,21 +1,32 @@ {% extends "base.html" %} -{% block title %}Explore 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 title %}Search Public Home Poker Leagues · 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 %} -
-

Public leagues

-

Explore

-

Browse poker leagues that have made their results publicly visible.

+
+
+

Public leagues

+

Search public leagues

+

Public league pages are discoverable by name, but myboker does not list every public league by default.

+
+ +
+ + + + {% if q %}Clear{% endif %} +
+ +
+ + Leagues are private unless an owner explicitly makes results public. +
+
+ +{% if has_searched and leagues %} +
+ {{ leagues|length }} result{{ 's' if leagues|length != 1 else '' }} for "{{ q }}"
- -
- - - {% if q %}Clear{% endif %} -
- -{% if leagues %}
{% for league in leagues %} {% set c = counts.get(league.id, {}) %} @@ -49,16 +60,16 @@ {% endfor %}
-{% elif q %} -
-

No results for "{{ q }}"

-

Try a shorter search or browse all leagues.

- Browse all +{% elif has_searched %} +
+

No public leagues found

+

No public league matched "{{ q }}". Check the spelling or ask the league owner for their shared link.

+ Reset search
{% else %} -
-

No public leagues yet

-

League owners can set their league to public in league settings.

+
+

Enter a league name to search

+

This page does not expose a directory of every public league. Search only returns leagues whose owners have chosen public visibility.

{% endif %} diff --git a/tests/test_seo.py b/tests/test_seo.py index e0967f2..3c87ad6 100644 --- a/tests/test_seo.py +++ b/tests/test_seo.py @@ -3,6 +3,7 @@ import unittest from pathlib import Path from app import create_app +from boker.auth import hash_password from boker.db import db @@ -60,6 +61,58 @@ class SeoTests(unittest.TestCase): self.assertIn("https://myboker.org/explore", body) self.assertIn("https://myboker.org/help", 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__": unittest.main()