require search first in explore
This commit is contained in:
5 files changed
+190
-43
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"
|
||||
|
||||
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",
|
||||
|
||||
+11
-15
@@ -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 []
|
||||
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}
|
||||
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")
|
||||
|
||||
+95
-12
@@ -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
|
||||
|
||||
+25
-14
@@ -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 %}
|
||||
|
||||
<section class="explore-hero">
|
||||
<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>
|
||||
<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">
|
||||
<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>
|
||||
{% if q %}<a class="btn btn--ghost" href="{{ url_for('public.explore') }}">Clear</a>{% endif %}
|
||||
</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">
|
||||
{% 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 %}
|
||||
|
||||
|
||||
@@ -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()
|
||||
Reference in new issue
Block a user