From 0f4d1652017bc7d6fab7219705748e826682a812 Mon Sep 17 00:00:00 2001 From: matt Date: Fri, 17 Oct 2025 18:40:15 -0700 Subject: [PATCH] feat(ui): add similar cards refresh button and reduce sidebar animation distractions --- CHANGELOG.md | 7 +- RELEASE_NOTES_TEMPLATE.md | 7 +- code/web/routes/card_browser.py | 83 +++++++++++++++++++ code/web/static/styles.css | 7 ++ code/web/templates/base.html | 46 +++++++++- .../browse/cards/_similar_cards.html | 63 +++++++++++++- 6 files changed, 204 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ef7d5ca..b59d05b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,13 +9,14 @@ This format follows Keep a Changelog principles and aims for Semantic Versioning ## [Unreleased] ### Summary -_No unreleased changes yet_ +Improved similar cards section with refresh button and reduced sidebar animation distractions. ### Added -_None_ +- Similar cards now have a refresh button to see different recommendations without reloading the page +- Explanation text clarifying that similarities are based on shared themes and tags ### Changed -_None_ +- Sidebar generally no longer animates during page loads and partial updates, reducing visual distractions ### Removed _None_ diff --git a/RELEASE_NOTES_TEMPLATE.md b/RELEASE_NOTES_TEMPLATE.md index 39fbda5..8ccc05b 100644 --- a/RELEASE_NOTES_TEMPLATE.md +++ b/RELEASE_NOTES_TEMPLATE.md @@ -1,13 +1,14 @@ # MTG Python Deckbuilder ${VERSION} ### Summary -_No unreleased changes yet_ +Improved similar cards section with refresh button and reduced sidebar animation distractions. ### Added -_None_ +- Similar cards now have a refresh button to see different recommendations without reloading the page +- Explanation text clarifying that similarities are based on shared themes and tags ### Changed -_None_ +- Sidebar generally no longer animates during page loads and partial updates, reducing visual distractions ### Removed _None_ diff --git a/code/web/routes/card_browser.py b/code/web/routes/card_browser.py index f5f5656..ba1edd7 100644 --- a/code/web/routes/card_browser.py +++ b/code/web/routes/card_browser.py @@ -1271,3 +1271,86 @@ async def card_detail(request: Request, card_name: str): ) +@router.get("/{card_name}/similar") +async def get_similar_cards_partial(request: Request, card_name: str): + """ + HTMX endpoint: Returns just the similar cards section for a given card. + Used for refreshing similar cards without reloading the entire page. + """ + try: + from urllib.parse import unquote + + # Decode URL-encoded card name + card_name = unquote(card_name) + + # Load cards data + loader = get_loader() + df = loader.load() + + # Get main card for theme tags + card_row = df[df['name'] == card_name] + if card_row.empty: + return templates.TemplateResponse( + "browse/cards/_similar_cards.html", + { + "request": request, + "similar_cards": [], + "main_card_tags": [], + } + ) + + card = card_row.iloc[0].to_dict() + main_card_tags = parse_theme_tags(card.get('themeTags', '')) + + # Calculate similar cards + similarity = get_similarity() + similar_cards = similarity.find_similar( + card_name, + threshold=0.8, + limit=5, + min_results=3, + adaptive=True + ) + + # Enrich similar cards with full data + for similar in similar_cards: + similar_row = df[df['name'] == similar['name']] + if not similar_row.empty: + similar_data = similar_row.iloc[0].to_dict() + theme_tags_parsed = parse_theme_tags(similar_data.get('themeTags', '')) + similar.update(similar_data) + similar['themeTags'] = theme_tags_parsed + + logger.info(f"Similar cards refresh for '{card_name}': {len(similar_cards)} cards") + + return templates.TemplateResponse( + "browse/cards/_similar_cards.html", + { + "request": request, + "card": card, + "similar_cards": similar_cards, + "main_card_tags": main_card_tags, + } + ) + + except Exception as e: + logger.error(f"Error loading similar cards for '{card_name}': {e}", exc_info=True) + # Try to get card data for error case too + try: + loader = get_loader() + df = loader.load() + card_row = df[df['name'] == card_name] + card = card_row.iloc[0].to_dict() if not card_row.empty else {"name": card_name} + except Exception: + card = {"name": card_name} + + return templates.TemplateResponse( + "browse/cards/_similar_cards.html", + { + "request": request, + "card": card, + "similar_cards": [], + "main_card_tags": [], + } + ) + diff --git a/code/web/static/styles.css b/code/web/static/styles.css index 02cc051..eda7352 100644 --- a/code/web/static/styles.css +++ b/code/web/static/styles.css @@ -125,6 +125,13 @@ body.nav-collapsed .top-banner .top-inner{ grid-template-columns: auto 1fr; } body.nav-collapsed .top-banner .top-inner{ padding-left: .5rem; padding-right: .5rem; } /* Smooth hide/show on mobile while keeping fixed positioning */ .sidebar{ transition: transform .2s ease-out, visibility .2s linear; } +/* Suppress sidebar transitions during page load to prevent pop-in */ +body.no-transition .sidebar{ transition: none !important; } +/* Suppress sidebar transitions during HTMX partial updates to prevent distracting animations */ +body.htmx-settling .sidebar{ transition: none !important; } +body.htmx-settling .layout{ transition: none !important; } +body.htmx-settling .content{ transition: none !important; } +body.htmx-settling *{ transition-duration: 0s !important; } /* Mobile tweaks */ @media (max-width: 900px){ diff --git a/code/web/templates/base.html b/code/web/templates/base.html index f0c014d..72996c3 100644 --- a/code/web/templates/base.html +++ b/code/web/templates/base.html @@ -39,6 +39,16 @@ window.__telemetryEndpoint = '/telemetry/events'; + @@ -50,7 +60,7 @@ {% endif %} - +
@@ -239,6 +249,7 @@ var SIDEBAR = document.getElementById('sidebar'); var TOGGLE = document.getElementById('nav-toggle'); var KEY = 'mtg:navCollapsed'; + function apply(collapsed){ if (collapsed){ BODY.classList.add('nav-collapsed'); @@ -254,6 +265,22 @@ var saved = localStorage.getItem(KEY); var initialCollapsed = (saved === '1') || (saved === null && (window.innerWidth || 0) < 900); apply(initialCollapsed); + + // Re-enable transitions after page is fully loaded + // Use longer delay for pages with heavy content (like card browser) + var enableTransitions = function(){ + BODY.classList.remove('no-transition'); + }; + + if (document.readyState === 'complete') { + // Already loaded + setTimeout(enableTransitions, 150); + } else { + window.addEventListener('load', function(){ + setTimeout(enableTransitions, 150); + }); + } + if (TOGGLE){ TOGGLE.addEventListener('click', function(){ var isCollapsed = BODY.classList.contains('nav-collapsed'); @@ -269,6 +296,23 @@ }); }catch(_){ } + // Suppress sidebar transitions during HTMX partial updates (not full page loads) + document.addEventListener('htmx:beforeRequest', function(evt){ + // Only suppress for small partial updates (identified by specific IDs) + var target = evt.detail && evt.detail.target; + if (target && target.id) { + var targetId = target.id; + // List of partial update containers that should suppress sidebar transitions + var partialContainers = ['similar-cards-container', 'card-list', 'theme-list']; + if (partialContainers.indexOf(targetId) !== -1 || targetId.indexOf('-container') !== -1) { + document.body.classList.add('htmx-settling'); + } + } + }); + document.addEventListener('htmx:afterSettle', function(){ + document.body.classList.remove('htmx-settling'); + }); + // Setup/Tagging status poller var statusEl; function ensureStatusEl(){ diff --git a/code/web/templates/browse/cards/_similar_cards.html b/code/web/templates/browse/cards/_similar_cards.html index 514fd17..85ef3df 100644 --- a/code/web/templates/browse/cards/_similar_cards.html +++ b/code/web/templates/browse/cards/_similar_cards.html @@ -4,6 +4,7 @@ align-items: center; justify-content: space-between; margin-bottom: 1.5rem; + gap: 1rem; } .similar-cards-title { @@ -12,6 +13,46 @@ color: var(--text); } + .refresh-similar-btn { + display: inline-flex; + align-items: center; + gap: 0.5rem; + padding: 0.6rem 1rem; + background: var(--panel); + color: var(--text); + border: 1px solid var(--border); + border-radius: 8px; + font-size: 0.9rem; + font-weight: 500; + cursor: pointer; + transition: all 0.2s; + white-space: nowrap; + } + + .refresh-similar-btn:hover { + background: var(--ring); + color: white; + border-color: var(--ring); + transform: translateY(-1px); + } + + .refresh-similar-btn svg { + transition: transform 0.3s; + } + + .refresh-similar-btn:hover svg { + transform: rotate(180deg); + } + + .refresh-similar-btn.htmx-request svg { + animation: spin 0.6s linear infinite; + } + + @keyframes spin { + from { transform: rotate(0deg); } + to { transform: rotate(360deg); } + } + .similar-cards-grid { display: grid; grid-template-columns: repeat(auto-fill, 280px); @@ -165,9 +206,27 @@ } -
+
-

Similar Cards

+
+

Similar Cards

+

+ Similarities based on shared themes and tags. Cards may differ in power level, cost, or function. +

+
+ {% if similar_cards and similar_cards|length > 0 %} + + {% endif %}
{% if similar_cards and similar_cards|length > 0 %}