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 %} -
++ Similarities based on shared themes and tags. Cards may differ in power level, cost, or function. +
+