"""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%}
{%config%}
{%scripts%}
{%renderer%}
'''
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",
)