require search first in explore

This commit is contained in:
SowinskiBraeden committed 2026-06-29 12:42:50 -07:00
1 parent 74d8baa6d7
commit 737d6d7561
5 files changed
+199 -52

No files matched your search

+6 -2
View File
@@ -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",
+12 -16
View File
@@ -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 = (
'<?xml version="1.0" encoding="UTF-8"?>\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
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")
+95 -12
View File
@@ -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
+33 -22
View File
@@ -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 %}
<div class="explore-header">
<p class="explore-header__eyebrow">Public leagues</p>
<h1 class="explore-header__title">Explore</h1>
<p class="explore-header__sub">Browse poker leagues that have made their results publicly visible.</p>
<section class="explore-hero">
<div class="explore-header">
<p class="explore-header__eyebrow">Public leagues</p>
<h1 class="explore-header__title">Search public leagues</h1>
<p class="explore-header__sub">Public league pages are discoverable by name, but myboker does not list every public league by default.</p>
</div>
<form method="get" action="{{ url_for('public.explore') }}" class="explore-search-row">
<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>
{% if q %}<a class="btn btn--ghost" href="{{ url_for('public.explore') }}">Clear</a>{% endif %}
</form>
<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>
<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">
<button class="btn btn--primary" type="submit">Search</button>
{% if q %}<a class="btn btn--ghost" href="{{ url_for('public.explore') }}">Clear</a>{% endif %}
</form>
{% if leagues %}
<div class="explore-cards">
{% for league in leagues %}
{% set c = counts.get(league.id, {}) %}
@@ -49,16 +60,16 @@
</a>
{% endfor %}
</div>
{% elif q %}
<div class="panel empty-panel" style="max-width:480px;">
<h3 class="panel__title">No results for "{{ q }}"</h3>
<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>
{% elif has_searched %}
<div class="explore-empty">
<h2>No public leagues found</h2>
<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') }}">Reset search</a>
</div>
{% else %}
<div class="panel empty-panel" style="max-width:480px;">
<h3 class="panel__title">No public leagues yet</h3>
<p class="muted-text">League owners can set their league to public in league settings.</p>
<div class="explore-empty explore-empty--start">
<h2>Enter a league name to search</h2>
<p>This page does not expose a directory of every public league. Search only returns leagues whose owners have chosen public visibility.</p>
</div>
{% endif %}
+53
View File
@@ -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("<loc>https://myboker.org/explore</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__":
unittest.main()