"""Gradio UI: builds the full multi-tab interface.""" import gradio as gr import data from data import load_matches, get_teams_from_matches from renderers import ( render_league_table_html, render_match_history_html, render_stat_cards, render_h2h_stats_html, get_h2h_match_history_html, make_status, update_score_preview, ) from crud import add_match, delete_match_by_id, update_match def build_interface(): """Build and return the Gradio Blocks demo.""" load_matches() initial_teams = get_teams_from_matches() with gr.Blocks(title="League Table Manager") as demo: # ── App header ────────────────────────────────── gr.HTML("""

⚽ League Table Manager

Track matches, standings, and head-to-head records

""") with gr.Tabs(): # ── Tab 1: Standings ───────────────────────── with gr.Tab("Standings"): gr.HTML("

Current Standings

") standings_html = gr.HTML(value=render_league_table_html(data.matches)) with gr.Accordion("Column Guide", open=False): gr.Markdown(""" | Column | Meaning | |--------|---------| | Win Rate | Win percentage (wins / games played) | | P | Matches played | | W | Wins | | D | Draws | | L | Losses | | GF | Goals scored (Goals For) | | GA | Goals conceded (Goals Against) | | GD | Goal difference (GF − GA) | | Pts | League points (W=3, D=1, L=0) | | G/GM | Goals scored per match | | WW | Whitewash wins (opponent scored 0) | | 5G | Matches where you scored 5 or more | """) # ── Tab 2: Add Match ───────────────────────── with gr.Tab("Add Match"): with gr.Row(): # Left: Add Match form with gr.Column(scale=1, min_width=280): gr.HTML("

Add Match

") score_preview = gr.HTML(value="""
Select teams and enter goals to preview
""") home_team = gr.Dropdown( choices=initial_teams, label="Home Team", value=initial_teams[0] if initial_teams else None, allow_custom_value=True ) away_team = gr.Dropdown( choices=initial_teams, label="Away Team", value=initial_teams[1] if len(initial_teams) > 1 else None, allow_custom_value=True ) with gr.Row(): home_goals = gr.Number(label="Home Goals", value=0, minimum=0, precision=0) away_goals = gr.Number(label="Away Goals", value=0, minimum=0, precision=0) submit_btn = gr.Button("Add Match", variant="primary") status_html = gr.HTML(value="") # Right: Match history with gr.Column(scale=2): gr.HTML("

Match History

") matches_html = gr.HTML(value=render_match_history_html(data.matches)) # Delete accordion with gr.Accordion("Delete a Match", open=False): gr.Markdown("Enter the row number from the history above, or click a row to auto-fill.") delete_preview_html = gr.HTML(value="") with gr.Row(): delete_row_input = gr.Number( label="Row # to Delete", value=None, minimum=1, precision=0, scale=2 ) stage_delete_btn = gr.Button("Preview Delete", variant="secondary", scale=1) confirm_delete_btn = gr.Button("Confirm Delete", variant="stop", visible=False) pending_match_id = gr.State(None) # Edit accordion with gr.Accordion("Edit a Match", open=False): gr.Markdown("Enter the row number to edit, fill in the new values, then click Save.") update_row_input = gr.Number(label="Row # to Edit", value=None, minimum=1, precision=0) with gr.Row(): update_home_team = gr.Dropdown( choices=initial_teams, label="New Home Team", allow_custom_value=True ) update_away_team = gr.Dropdown( choices=initial_teams, label="New Away Team", allow_custom_value=True ) with gr.Row(): update_home_goals = gr.Number(label="Home Goals", value=0, minimum=0, precision=0) update_away_goals = gr.Number(label="Away Goals", value=0, minimum=0, precision=0) update_btn = gr.Button("Save Changes", variant="secondary") update_status_html = gr.HTML(value="") # ── Score preview events ── for component in [home_team, away_team, home_goals, away_goals]: component.change( fn=update_score_preview, inputs=[home_team, away_team, home_goals, away_goals], outputs=[score_preview] ) # ── Add Match ── def add_match_full(home, away, hg, ag): status = add_match(home, away, hg, ag) teams = get_teams_from_matches() return ( render_league_table_html(data.matches), render_match_history_html(data.matches), make_status(status), gr.Number(value=0), gr.Number(value=0), gr.Dropdown(choices=teams), gr.Dropdown(choices=teams), gr.Dropdown(choices=teams), gr.Dropdown(choices=teams), ) submit_btn.click( fn=add_match_full, inputs=[home_team, away_team, home_goals, away_goals], outputs=[standings_html, matches_html, status_html, home_goals, away_goals, home_team, away_team, update_home_team, update_away_team], show_progress="minimal" ) # ── Two-step Delete ── def stage_delete(row_number): if row_number is None or row_number < 1: return ( '
✗ Invalid row number
', None, gr.update(visible=False) ) sorted_m = sorted(data.matches, key=lambda x: x[5], reverse=True) row_idx = int(row_number) - 1 if row_idx < 0 or row_idx >= len(sorted_m): return ( f'
✗ Row {int(row_number)} does not exist
', None, gr.update(visible=False) ) m = sorted_m[row_idx] preview = f"""
About to delete row {int(row_number)}: {m[1]} {m[3]} – {m[4]} {m[2]}
Click Confirm Delete to proceed.
""" return preview, m[0], gr.update(visible=True) stage_delete_btn.click( fn=stage_delete, inputs=[delete_row_input], outputs=[delete_preview_html, pending_match_id, confirm_delete_btn] ) def execute_delete(match_id): if match_id is None: return ( render_league_table_html(data.matches), render_match_history_html(data.matches), make_status("Error: No match staged for deletion"), "", None, gr.update(visible=False) ) ok = delete_match_by_id(match_id) status = "Match deleted successfully" if ok else "Error: Failed to delete match" return ( render_league_table_html(data.matches), render_match_history_html(data.matches), make_status(status), "", None, gr.update(visible=False) ) confirm_delete_btn.click( fn=execute_delete, inputs=[pending_match_id], outputs=[standings_html, matches_html, status_html, delete_preview_html, pending_match_id, confirm_delete_btn], show_progress="minimal" ) # ── Edit Match ── def update_match_full(row_number, new_home, new_away, new_home_goals, new_away_goals): status = update_match(row_number, new_home, new_away, new_home_goals, new_away_goals) teams = get_teams_from_matches() return ( render_league_table_html(data.matches), render_match_history_html(data.matches), make_status(status), gr.Dropdown(choices=teams), gr.Dropdown(choices=teams), gr.Dropdown(choices=teams), gr.Dropdown(choices=teams), ) update_btn.click( fn=update_match_full, inputs=[update_row_input, update_home_team, update_away_team, update_home_goals, update_away_goals], outputs=[standings_html, matches_html, update_status_html, home_team, away_team, update_home_team, update_away_team], show_progress="minimal" ) # ── Tab 3: Head to Head ────────────────────── with gr.Tab("Head to Head"): gr.HTML("

Head-to-Head Comparison

") with gr.Row(): h2h_team1 = gr.Dropdown( choices=initial_teams, label="Team 1", value=initial_teams[0] if initial_teams else None, allow_custom_value=True, scale=1 ) h2h_team2 = gr.Dropdown( choices=initial_teams, label="Team 2", value=initial_teams[1] if len(initial_teams) > 1 else None, allow_custom_value=True, scale=1 ) h2h_stats_html = gr.HTML( value=render_h2h_stats_html( initial_teams[0] if initial_teams else None, initial_teams[1] if len(initial_teams) > 1 else None, data.matches ) ) gr.HTML("

Match History

") h2h_history_html = gr.HTML( value=get_h2h_match_history_html( initial_teams[0] if initial_teams else None, initial_teams[1] if len(initial_teams) > 1 else None, data.matches ) ) def update_h2h(t1, t2): return ( render_h2h_stats_html(t1, t2, data.matches), get_h2h_match_history_html(t1, t2, data.matches) ) h2h_team1.change(fn=update_h2h, inputs=[h2h_team1, h2h_team2], outputs=[h2h_stats_html, h2h_history_html]) h2h_team2.change(fn=update_h2h, inputs=[h2h_team1, h2h_team2], outputs=[h2h_stats_html, h2h_history_html]) # ── Tab 4: Records ─────────────────────────── with gr.Tab("Records"): gr.HTML("

League Records

") records_html = gr.HTML(value=render_stat_cards(data.matches)) # ── Page load refresh ── def refresh_all(): load_matches() teams = get_teams_from_matches() t1 = teams[0] if teams else None t2 = teams[1] if len(teams) > 1 else None return ( render_league_table_html(data.matches), render_match_history_html(data.matches), render_h2h_stats_html(t1, t2, data.matches), get_h2h_match_history_html(t1, t2, data.matches), render_stat_cards(data.matches), gr.Dropdown(choices=teams), gr.Dropdown(choices=teams), gr.Dropdown(choices=teams), gr.Dropdown(choices=teams), gr.Dropdown(choices=teams), gr.Dropdown(choices=teams), ) demo.load( fn=refresh_all, inputs=[], outputs=[ standings_html, matches_html, h2h_stats_html, h2h_history_html, records_html, home_team, away_team, update_home_team, update_away_team, h2h_team1, h2h_team2, ] ) return demo