diff --git a/.nicegui/storage-user-83ffc178-0f94-4ada-8ca6-1c51b99b4b9c.json b/.nicegui/storage-user-83ffc178-0f94-4ada-8ca6-1c51b99b4b9c.json index fc64305..8c7f079 100644 --- a/.nicegui/storage-user-83ffc178-0f94-4ada-8ca6-1c51b99b4b9c.json +++ b/.nicegui/storage-user-83ffc178-0f94-4ada-8ca6-1c51b99b4b9c.json @@ -1 +1 @@ -{"authenticated":true,"discord_id":"277898241750859776","discord_name":"mrteels","db_id":1,"display_name":"Chaos Regelvergesser","discord_avatar_url":"https://cdn.discordapp.com/avatars/277898241750859776/7c3446bb51fafd72b1b4c21124b4994f.png"} \ No newline at end of file +{"authenticated":true,"discord_id":"277898241750859776","discord_name":"mrteels","db_id":2,"display_name":"Zorniger Grot","discord_avatar_url":"https://cdn.discordapp.com/avatars/277898241750859776/7c3446bb51fafd72b1b4c21124b4994f.png"} \ No newline at end of file diff --git a/data/data_api.py b/data/data_api.py index 983773b..9388986 100644 --- a/data/data_api.py +++ b/data/data_api.py @@ -64,6 +64,36 @@ def get_all_players(): return result +def get_all_players_from_system(system_name): + connection = sqlite3.connect(DB_PATH) + connection.row_factory = sqlite3.Row + cursor = connection.cursor() + + # ID und Namen der Spieler. + # DISTINCT stellt sicher, dass jeder Spieler nur einmal vorkommt. + query = """ + SELECT DISTINCT + p.id AS player_id, + p.display_name, + p.discord_name + FROM players p + JOIN player_game_statistic stat ON p.id = stat.player_id + JOIN gamesystems sys ON stat.gamesystem_id = sys.id + WHERE sys.name = ? + ORDER BY p.display_name ASC + """ + + cursor.execute(query, (system_name,)) + rows = cursor.fetchall() + connection.close() + + result = [] + for row in rows: + result.append(dict(row)) + + return result + + import sqlite3 from data.setup_database import DB_PATH @@ -127,4 +157,70 @@ def join_league(player_id, gamesystem_id): connection.commit() connection.close() - + + +def get_recent_matches_for_player(player_id): + connection = sqlite3.connect(DB_PATH) + connection.row_factory = sqlite3.Row + cursor = connection.cursor() + + query = """ + SELECT + m.id AS match_id, + sys.name AS gamesystem_name, + m.player1_id, + p1.display_name AS p1_display, + p1.discord_name AS p1_discord, + m.score_player1, + m.player2_id, + p2.display_name AS p2_display, + p2.discord_name AS p2_discord, + m.score_player2, + m.played_at + FROM matches m + JOIN gamesystems sys ON m.gamesystem_id = sys.id + JOIN players p1 ON m.player1_id = p1.id + JOIN players p2 ON m.player2_id = p2.id + WHERE m.player1_id = ? OR m.player2_id = ? + ORDER BY m.played_at DESC + LIMIT 10 + """ + + cursor.execute(query, (player_id, player_id)) + rows = cursor.fetchall() + connection.close() + + result = [] + for row in rows: + result.append(dict(row)) + + return result + + +def add_new_match(system_name, player1_id, player2_id, score_p1, score_p2): + connection = sqlite3.connect(DB_PATH) + cursor = connection.cursor() + + # 1. Wir suchen die interne ID des Spielsystems anhand des Namens (z.B. "Warhammer 40k" -> 1) + cursor.execute("SELECT id FROM gamesystems WHERE name = ?", (system_name,)) + sys_row = cursor.fetchone() + + # Sicherheitscheck (sollte eigentlich nie passieren) + if not sys_row: + connection.close() + return False + + sys_id = sys_row[0] + + # 2. Das Match eintragen! (Datum 'played_at' macht SQLite durch DEFAULT CURRENT_TIMESTAMP automatisch) + query = """ + INSERT INTO matches (gamesystem_id, player1_id, player2_id, score_player1, score_player2) + VALUES (?, ?, ?, ?, ?) + """ + + cursor.execute(query, (sys_id, player1_id, player2_id, score_p1, score_p2)) + + connection.commit() + connection.close() + + return True diff --git a/data/setup_database.py b/data/setup_database.py index 11bb455..8d65ffd 100644 --- a/data/setup_database.py +++ b/data/setup_database.py @@ -105,7 +105,7 @@ def seed_gamesystems(): cursor = connection.cursor() systems = [ ("Warhammer 40k","/gui/pictures/wsdg.png","Die Schlacht um die Galaxie in einer entfernten Zukunft." , 0, 100), - ("Age of Sigmar","/gui/pictures/wsdg.png","" , 0, 50), + ("Warhammer Age of Sigmar","/gui/pictures/wsdg.png","" , 0, 50), ("Spearhead","/gui/pictures/wsdg.png","" , 0, 50) ] @@ -135,5 +135,47 @@ def seed_achievements(): connection.commit() connection.close() print("Achievements angelegt.") + seed_dummy_player() +def seed_dummy_player(): + connection = sqlite3.connect(DB_PATH) + cursor = connection.cursor() + + # 1. Dummy-Spieler anlegen (falls noch nicht vorhanden) + query_player = """ + INSERT OR IGNORE INTO players + (discord_id, discord_name, display_name, discord_avatar_url) + VALUES (?, ?, ?, ?) + """ + cursor.execute(query_player, ("dummy_001", "dummy_user", "Dummy Mc DummDumm", "")) + + # 2. Wir holen uns die ID des Dummys (egal ob neu oder alt) + cursor.execute("SELECT id FROM players WHERE discord_id = 'dummy_001'") + dummy_row = cursor.fetchone() + + if dummy_row: + dummy_id = dummy_row[0] + + # 3. Wir holen alle IDs der aktuellen Spielsysteme + cursor.execute("SELECT id FROM gamesystems") + systems = cursor.fetchall() + + # 4. Wir gehen jedes System durch und prüfen, ob er schon drin ist + for sys in systems: + sys_id = sys[0] + + cursor.execute("SELECT id FROM player_game_statistic WHERE player_id = ? AND gamesystem_id = ?", (dummy_id, sys_id)) + is_in_league = cursor.fetchone() + + if not is_in_league: + # Er ist noch nicht drin -> Eintragen! (MMR startet durch DEFAULT automatisch bei 1000) + cursor.execute(""" + INSERT INTO player_game_statistic (player_id, gamesystem_id) + VALUES (?, ?) + """, (dummy_id, sys_id)) + + connection.commit() + connection.close() + print("Test-Gegner 'Dummy Mc DummDumm' ist bereit und in allen Ligen angemeldet!") + diff --git a/gui/league_statistic.py b/gui/league_statistic.py index f58496b..ee04f36 100644 --- a/gui/league_statistic.py +++ b/gui/league_statistic.py @@ -1,11 +1,11 @@ from nicegui import ui, app from gui import gui_style -def setup_statistic_routes(): - +def setup_routes(): # 1. Die {}-Klammern definieren eine dynamische Variable in der URL @ui.page('/statistic/{systemname}') def gamesystem_statistic_page(systemname: str): # <--- WICHTIG: Hier fangen wir das Wort aus der URL auf! + sys_name = systemname # Sicherheitscheck: Ist der User eingeloggt? if not app.storage.user.get('authenticated', False): @@ -13,14 +13,13 @@ def setup_statistic_routes(): return gui_style.apply_design() - print(systemname) with ui.header().classes('items-center justify-between bg-zinc-900 p-4 shadow-lg'): ui.button('Zurück zur Übersicht', on_click=lambda: ui.navigate.to('/')).classes('mt-8 bg-gray-600 text-white') - ui.button("Spiel eintragen", on_click=lambda: ui.navigate.to('/add-match/{systemname}')) + ui.button("Spiel eintragen", on_click=lambda: ui.navigate.to(f'/add-match/{sys_name}')) with ui.column().classes('w-full items-center mt-10'): # Hier nutzen wir die Variable 'systemname', um den Titel anzupassen - ui.label(f'Deine Statistik in {systemname}').classes('text-3xl font-bold text-blue-400') + ui.label(f'Deine Statistik in {sys_name}').classes('text-3xl font-bold text-blue-400') with ui.card().classes('w-full max-w-2xl mt-6 p-6 items-center'): ui.label('Hier laden wir im nächsten Schritt die Daten aus der player_game_statistic Tabelle!').classes('text-gray-400') diff --git a/gui/main_gui.py b/gui/main_gui.py index a86554e..9c2be64 100644 --- a/gui/main_gui.py +++ b/gui/main_gui.py @@ -7,7 +7,10 @@ def setup_routes(): def home_page(): gui_style.apply_design() + # --------------------------- # --- NAVIGATIONSLEISTE (HEADER) --- + # --------------------------- + with ui.header().classes('items-center justify-between bg-zinc-900 p-4 shadow-lg'): # --- LINKE SEITE --- @@ -23,7 +26,7 @@ def setup_routes(): display_name = app.storage.user.get('display_name') player_id = app.storage.user.get('db_id') - # 1. Wir definieren eine kleine Funktion, die zwischen Text und Eingabe hin- und herschaltet + # 1. kleine Funktion, die zwischen Text und Eingabe hin- und herschaltet def toggle_edit_mode(): display_row.visible = not display_row.visible edit_row.visible = not edit_row.visible @@ -34,7 +37,7 @@ def setup_routes(): ui.label('aka').classes('text-sm text-gray-500') ui.label(discord_name).classes('text-lg text-gray-400') - # Ein runder Button (.props('round')), der exakt wie ein FAB aussieht! + # Ein runder Button (.props('round')) ui.button(icon='edit', color='primary', on_click=toggle_edit_mode).props('round dense') # --- ANSICHT 2: Das Eingabefeld (startet unsichtbar!) --- @@ -56,7 +59,6 @@ def setup_routes(): name_input = ui.input('Neuer Name', value=display_name).on('keydown.enter', save_new_name) ui.button(icon='save', color='positive', on_click=save_new_name).props('round dense') ui.button(icon='close', color='negative', on_click=toggle_edit_mode).props('round dense') - # --------------------------------------------- avatar = app.storage.user.get('avatar_url') if avatar: @@ -71,21 +73,28 @@ def setup_routes(): else: auth_url = discord_login.get_auth_url() ui.button('Login with Discord', on_click=lambda: ui.navigate.to(auth_url)) - # ---------------------------------- + # --------------------------- # --- Spielsysteme --- + # --------------------------- + if app.storage.user.get('authenticated', False): with ui.card().classes("w-full"): ui.label(text="Meine Ligaplätze").classes("font-bold text-white text-xl") placements = data_api.get_player_statistics(player_id) systems = data_api.get_gamesystem_data() - + def click_join_league(p_id, sys_id): data_api.join_league(p_id, sys_id) ui.navigate.to("/") + # --- NEU: Wir machen die Funktion allgemein --- + def toggle_visibility(row_a, row_b): + row_a.visible = not row_a.visible + row_b.visible = not row_b.visible + with ui.grid(columns=3).classes("w-full gap-4 items-center"): for place in placements: sys_card = ui.card().classes("h-60 w-full items-center justify-center transition-colors") @@ -102,11 +111,20 @@ def setup_routes(): if place['mmr'] is None: ui.label(text="Du bist noch nicht in dieser Liga.").classes("text-red-500 font-bold") - # --- HIER IST DER MAGISCHE TRICK --- - # Das 'e' fängt das NiceGUI-Event ab und wirft es weg. - # p=player_id und s=place['gamesystem_id'] frieren die IDs für DIESEN Button bombenfest ein! - ui.button("Liga Beitreten", on_click=lambda e, p=player_id, s=place['gamesystem_id']: click_join_league(p, s)) + # 1. Wir legen die Reihen an und speichern sie in lokalen Variablen + join_row = ui.row().classes('items-center gap-2') + confirm_row = ui.row().classes('items-center gap-2') + confirm_row.visible = False # Standardmäßig unsichtbar + # 2. Erste Reihe (Der "Beitreten" Button) + with join_row: + ui.button("Beitreten", on_click=lambda e, r1=join_row, r2=confirm_row: toggle_visibility(r1, r2)) + + # 3. Zweite Reihe (Die Bestätigungs-Buttons) + with confirm_row: + ui.button("Liga Beitreten", color="green", on_click=lambda e, p=player_id, s=place['gamesystem_id']: click_join_league(p, s)) + # Der Abbrechen Button kriegt den gleichen Toggle-Befehl wie oben: + ui.button(icon='cancel', color='red', on_click=lambda e, r1=join_row, r2=confirm_row: toggle_visibility(r1, r2)).props('round dense') else: # Spieler IST in der Liga! sys_card.classes("cursor-pointer hover:bg-zinc-800") @@ -118,7 +136,64 @@ def setup_routes(): ui.label(text=f"MMR: {place['mmr']}") ui.label(text=f"Spiele: {place['games_in_system']}") ui.label(text=f"Punkte: {place['points']}") - + + # --------------------------- + # Match Historie + # --------------------------- + + with ui.card().classes("w-full"): + ui.label(text= "Meine letzten Spiele").classes("font-bold text-white text-xl") + # 1. Daten aus der DB holen + raw_matches = data_api.get_recent_matches_for_player(player_id) + + # 2. Daten für die Tabelle aufbereiten + table_rows = [] + for match in raw_matches: + # Bin ich Spieler 1 oder Spieler 2? + if match['player1_id'] == player_id: + # Ich bin Spieler 1, also ist Spieler 2 der Gegner + opponent_name = f"{match['p2_display']} aka {match['p2_discord']}" + my_score = match['score_player1'] + opp_score = match['score_player2'] + else: + # Ich bin Spieler 2, also ist Spieler 1 der Gegner + opponent_name = f"{match['p1_display']} aka {match['p1_discord']}" + my_score = match['score_player2'] + opp_score = match['score_player1'] + + # Gewonnen oder Verloren? + if my_score > opp_score: + result_text = "Gewonnen" + elif my_score < opp_score: + result_text = "Verloren" + else: + result_text = "Unentschieden" + + # Datum hübsch machen (schneidet die Millisekunden weg) + date_clean = str(match['played_at'])[:10] + + # Eine fertige Zeile für unsere Tabelle bauen + table_rows.append({ + 'date': date_clean, + 'system': match['gamesystem_name'], + 'opponent': opponent_name, + 'result': f"{result_text} ({my_score} : {opp_score})" + }) + + # 3. Das Layout (Die Spalten) der Tabelle definieren + table_columns = [ + {'name': 'date', 'label': 'Gespielt am', 'field': 'date', 'align': 'left'}, + {'name': 'system', 'label': 'System', 'field': 'system', 'align': 'left'}, + {'name': 'opponent', 'label': 'Gegner', 'field': 'opponent', 'align': 'left'}, + {'name': 'result', 'label': 'Ergebnis', 'field': 'result', 'align': 'left'} + ] + + # 4. Die Tabelle zeichnen! + if len(table_rows) > 0: + ui.table(columns=table_columns, rows=table_rows, row_key='date').classes('w-full bg-zinc-900 text-white') + else: + ui.label("Noch keine Spiele absolviert.").classes("text-gray-500 italic") + diff --git a/gui/match_gui.py b/gui/match_gui.py index a0bdadc..e8c81ab 100644 --- a/gui/match_gui.py +++ b/gui/match_gui.py @@ -1,7 +1,8 @@ from nicegui import ui, app from gui import gui_style +from data import data_api -def setup_match_routes(): +def setup_routes(): # 1. Die {}-Klammern definieren eine dynamische Variable in der URL @ui.page('/add-match/{systemname}') @@ -15,11 +16,62 @@ def setup_match_routes(): ui.button('Back to Home', on_click=lambda: ui.navigate.to('/')) return - with ui.card().classes('w-full items-center mt-10'): + with ui.card().classes('w-150 items-center mt-10 justify-center'): ui.label('Neues Spiel für '+ systemname + " eintragen").classes('text-2xl font-bold') - # --- PLATZHALTER --- - ui.label('Hier kommt im nächsten Schritt das Eingabe-Formular hin!').classes('text-gray-500 my-4') + ui.space() - # Ein Button, um wieder zurück zur Startseite zu kommen - ui.button('Cancel', on_click=lambda: ui.navigate.to('/')).classes('bg-gray-500 text-white') + ui.label("Meine Punkte:").classes('text-xl font-bold') + + ui.space() + + with ui.column().classes("w-full items-center h-60"): + # 1. Daten aus der DB holen + raw_players = data_api.get_all_players_from_system(systemname) + my_id = app.storage.user.get('db_id') + + # 3. Eine saubere Optionen-Liste (Dictionary) für NiceGUI bauen + dropdown_options = {} + + for p in raw_players: + # FILTERN: Wenn die ID des Spielers MEINE EIGENE ID ist, überspringen wir ihn! + if p['player_id'] == my_id: + continue # Geht sofort zum nächsten Spieler in der Schleife + + # Die ID wird der Schlüssel, der kombinierte Name wird der Anzeigetext + # Ergebnis sieht z.B. so aus: { 2: "Dummy Mc DummDumm aka dummy_user" } + dropdown_options[p['player_id']] = f"{p['display_name']} aka {p['discord_name']}" + + # 4. Eigene Punkte eintragen + p1_points = ui.slider(min= 0, max=100, value = 10).props("label-always") + + ui.space() + + # 5. Das Dropdown mit unseren sauberen Optionen füttern + ui.label("Gegner:").classes('text-xl font-bold') + opponent_select = ui.select(options=dropdown_options, label='Gegner auswählen').classes('w-64') + p2_points = ui.slider(min= 0, max=100, value = 10).props("label-always") + + ui.space() + + def input_match_to_database(): + # 1. Prüfen, ob ein Gegner ausgewählt wurde + p2_id = opponent_select.value + if p2_id is None: + ui.notify("Bitte wähle zuerst einen Gegner aus!", color="red", position="top") + return # Bricht die Funktion hier ab! + + # 2. Punkte von den Slidern holen + score_p1 = p1_points.value + score_p2 = p2_points.value + + # 3. Daten an die API schicken (my_id haben wir oben schon von app.storage.user geholt) + data_api.add_new_match(systemname, my_id, p2_id, score_p1, score_p2) + + # 4. Erfolgsmeldung und zurück zur Übersicht + ui.notify("Match erfolgreich eingetragen!", color="green") + ui.navigate.to('/') + + with ui.row().classes("w-full items-center justify-between"): + ui.button('Cancel', on_click=lambda: ui.navigate.to('/')).classes('bg-gray-500 text-white') + ui.button(text="Absenden", color="positive", on_click=lambda: input_match_to_database()).classes("") diff --git a/main.py b/main.py index e88be88..2f7002a 100644 --- a/main.py +++ b/main.py @@ -20,9 +20,9 @@ client_secret = os.getenv("DISCORD_CLIENT_SECRET") # 3. Seitenrouten aufbauen main_gui.setup_routes() -match_gui.setup_match_routes() discord_login.setup_login_routes() -league_statistic.setup_statistic_routes() +league_statistic.setup_routes() +match_gui.setup_routes() # 4. Wir starten die NiceGUI App ui.run(title="Warhammer Liga", port=9000, storage_secret="ein_sehr_geheimes_passwort_fuer_die_cookies")