"""Dating-app themed tournament UI for g-Harmony.""" import random from dash import dcc, html import dash_bootstrap_components as dbc from src.galaxy_profiles import GALAXY_PROFILES, GALAXY_IDS, NUM_GALAXIES def get_app_theme() -> str: """Return the full HTML template with embedded CSS.""" return ''' {%metas%} g-Harmony {%favicon%} {%css%} {%app_entry%} ''' def _create_star_field(n=80): """Generate CSS star-field background as inline-styled divs.""" stars = [] for i in range(n): x = random.random() * 100 y = random.random() * 100 size = random.random() * 2 + 0.5 delay = round(random.random() * 4, 1) duration = round(random.random() * 3 + 2, 1) stars.append(html.Div(style={ "position": "absolute", "left": f"{x:.1f}%", "top": f"{y:.1f}%", "width": f"{size:.1f}px", "height": f"{size:.1f}px", "borderRadius": "50%", "background": "#fff", "animation": f"twinkle {duration}s {delay}s infinite ease-in-out", })) return html.Div( stars, style={ "position": "fixed", "top": "0", "left": "0", "right": "0", "bottom": "0", "pointerEvents": "none", "zIndex": "0", }, ) def create_galaxy_card(galaxy_id, side="left", is_champion=False): """Build a single galaxy profile card -- image + name + description.""" profile = GALAXY_PROFILES[galaxy_id] btn_id = f"{side}-card-btn" card_style = { "border": "none", "padding": "0", "textAlign": "left", "width": "100%", } # Add champion styling if is_champion: card_style["boxShadow"] = "0 0 20px rgba(255, 215, 0, 0.5)" card_style["border"] = "2px solid rgba(255, 215, 0, 0.7)" card_style["borderRadius"] = "12px" card_style["animation"] = "galaxyWin 1.5s ease-in-out" # Get description text description = profile.get('description', profile.get('bio', '')) card_contents = [ html.Img( src=f"/galaxy-images/{galaxy_id}.jpg", className="galaxy-card-image", ), html.Div( [ html.Span(profile["name"], style={"marginRight": "8px"}), html.I( className="fas fa-crown", style={ "color": "#FFD700", "fontSize": "0.9rem", "display": "inline" if is_champion else "none", } ), ], className="galaxy-card-name", ), # Add description below the name html.Div( description, className="galaxy-card-description", style={ "fontSize": "0.8rem", "color": "rgba(255,255,255,0.7)", "padding": "8px 16px 16px", "lineHeight": "1.4", "fontFamily": "'Outfit', sans-serif", } ) if description else None, ] # Filter out None elements card_contents = [item for item in card_contents if item is not None] return html.Button( card_contents, className="galaxy-card", id=btn_id, n_clicks=0, style=card_style, ) def create_arena(left_id=None, right_id=None, champion_id=None): """Build the two-card arena with VS divider.""" if left_id is None or right_id is None: # All done state return html.Div( [ html.Div( "Champion Reign Complete!", style={ "fontFamily": "'Playfair Display', serif", "fontSize": "1.8rem", "fontWeight": "700", "color": "#fff", "marginBottom": "12px", }, ), html.P( "The current champion has faced all possible challengers. " "Check the leaderboard below for the final rankings!", style={"color": "rgba(255,255,255,0.5)", "maxWidth": "400px", "margin": "0 auto 24px"}, ), html.Div( id="leaderboard-showcase", children=html.Div( "Click a leaderboard row to preview that galaxy here.", style={"color": "rgba(255,255,255,0.45)", "fontSize": "0.85rem"}, ), style={"marginBottom": "24px"}, ), dbc.Button( "Reset Session", id="reset-session", style={ "background": "linear-gradient(135deg, #a78bfa, #f472b6)", "border": "none", "color": "#fff", "fontFamily": "'Outfit', sans-serif", "fontWeight": "600", "padding": "12px 36px", "borderRadius": "30px", "fontSize": "0.95rem", }, ), ], className="all-done-card", ) # Determine champion status left_is_champion = champion_id is not None and left_id == champion_id right_is_champion = champion_id is not None and right_id == champion_id return dbc.Row( [ dbc.Col( create_galaxy_card(left_id, side="left", is_champion=left_is_champion), width=5, ), dbc.Col( html.Div("VS", className="vs-divider"), width=2, className="d-flex align-items-center justify-content-center", ), dbc.Col( create_galaxy_card(right_id, side="right", is_champion=right_is_champion), width=5, ), ], className="g-0 align-items-stretch", style={"animation": "fadeSlideUp 0.4s ease"}, ) def create_compact_showcase(galaxy_id): """Render a compact selected-galaxy preview for the all-done card.""" profile = GALAXY_PROFILES[galaxy_id] description = profile.get("description", profile.get("bio", "")) return html.Div( [ html.Img( src=f"/galaxy-images/{galaxy_id}.jpg", style={ "width": "168px", "height": "168px", "objectFit": "cover", "borderRadius": "14px", "border": "2px solid rgba(255,255,255,0.18)", }, ), html.Div( profile["name"], style={ "marginTop": "10px", "fontFamily": "'Playfair Display', serif", "fontSize": "1.1rem", "fontWeight": "700", "color": "#fff", }, ), html.Div( description, style={ "marginTop": "6px", "maxWidth": "360px", "fontSize": "0.8rem", "lineHeight": "1.35", "color": "rgba(255,255,255,0.68)", }, ) if description else None, ], style={"display": "flex", "flexDirection": "column", "alignItems": "center", "animation": "fadeSlideUp 0.3s ease"}, ) def create_leaderboard_rows(leaderboard_data): """Build leaderboard row elements from sorted data.""" rows = [] for i, entry in enumerate(leaderboard_data): gid = entry["id"] profile = GALAXY_PROFILES.get(gid, {}) rank = i + 1 # Medal for top 3 rank_display = {1: "1", 2: "2", 3: "3"}.get(rank, str(rank)) rank_color = {1: "#FFD700", 2: "#C0C0C0", 3: "#CD7F32"}.get(rank, "rgba(255,255,255,0.4)") rows.append( html.Div( [ html.Span(rank_display, className="leaderboard-rank", style={"color": rank_color}), html.Img(src=f"/galaxy-images/{gid}.jpg", className="leaderboard-thumb"), html.Span(profile.get("name", gid[:8]), className="leaderboard-name"), html.Span(f"{entry['elo']:.0f}", className="leaderboard-elo"), ], className="leaderboard-row", id={"type": "leaderboard-row", "index": gid}, n_clicks=0, style={"cursor": "pointer"}, ) ) return rows def create_layout(): """Assemble the complete app layout.""" return dbc.Container( [ _create_star_field(80), # Header html.Div( [ html.Div("g-Harmony", className="gharmony-title text-center"), html.Div("FIND YOUR GALAXY MATCH", className="gharmony-tagline text-center mt-1"), html.Div( "Left/Right arrow keys to choose", style={ "fontFamily": "'Outfit', sans-serif", "fontSize": "0.65rem", "fontWeight": "400", "color": "rgba(255,255,255,0.2)", "letterSpacing": "1px", "marginTop": "8px", }, ), ], className="text-center pt-4 pb-3", style={"position": "relative", "zIndex": "10"}, ), # Arena html.Div(id="arena-container", style={"position": "relative", "zIndex": "10"}), # Spacer html.Div(style={"height": "32px"}), # Leaderboard html.Div( [ html.Div( [ html.Span("LEADERBOARD"), html.I(className="fas fa-chevron-down", id="leaderboard-arrow", style={"transition": "transform 0.3s", "fontSize": "0.65rem"}), ], className="leaderboard-header", id="leaderboard-toggle", n_clicks=0, ), html.Div(id="leaderboard-body", style={"display": "none"}), ], className="leaderboard-container mb-4", style={"position": "relative", "zIndex": "10"}, ), # Stores dcc.Store(id="seen-pairs", data=[]), dcc.Store(id="current-pair", data=None), dcc.Store(id="current-champion", data=None), dcc.Store(id="comparison-count", data=0), dcc.Store(id="session-id", data=""), ], fluid=True, className="py-0", )