2026-02-25 15:26:24 +01:00
|
|
|
from nicegui import ui, app
|
2026-03-02 10:16:30 +01:00
|
|
|
from data import database, data_api
|
2026-03-01 20:34:42 +01:00
|
|
|
from gui import discord_login, gui_style
|
2026-03-06 21:20:48 +01:00
|
|
|
from match_calculations import calc_match
|
2026-03-12 09:34:22 +01:00
|
|
|
from gui.info_text import info_system
|
2026-02-25 15:26:24 +01:00
|
|
|
|
2026-03-06 21:20:48 +01:00
|
|
|
def setup_routes(admin_discord_id):
|
2026-03-08 16:23:39 +01:00
|
|
|
@ui.page('/', dark=True)
|
2026-02-25 15:26:24 +01:00
|
|
|
def home_page():
|
2026-03-01 20:34:42 +01:00
|
|
|
gui_style.apply_design()
|
2026-02-25 15:26:24 +01:00
|
|
|
|
2026-03-09 13:05:11 +01:00
|
|
|
# --- SICHERHEITS-CHECK (Hassan, DER TÜRSTEHER) ---
|
|
|
|
|
if app.storage.user.get('authenticated', False):
|
|
|
|
|
db_id = app.storage.user.get('db_id')
|
|
|
|
|
discord_id = app.storage.user.get('discord_id')
|
|
|
|
|
|
|
|
|
|
# Fehlt die Discord-ID (altes Cookie) ODER sagt die Datenbank, dass da was nicht stimmt?
|
|
|
|
|
if not discord_id or not data_api.validate_user_session(db_id, discord_id):
|
|
|
|
|
# Ausweis ungültig! Wir vernichten das Cookie sofort.
|
|
|
|
|
app.storage.user.clear()
|
|
|
|
|
ui.notify("Deine Sitzung ist ungültig oder abgelaufen. Bitte neu einloggen!", color="negative")
|
|
|
|
|
|
|
|
|
|
ui.navigate.reload()
|
|
|
|
|
return
|
|
|
|
|
# -----------------------------------------
|
|
|
|
|
|
2026-03-03 15:49:40 +01:00
|
|
|
# ---------------------------
|
2026-02-27 12:01:02 +01:00
|
|
|
# --- NAVIGATIONSLEISTE (HEADER) ---
|
2026-03-03 15:49:40 +01:00
|
|
|
# ---------------------------
|
|
|
|
|
|
2026-03-12 15:23:09 +01:00
|
|
|
with ui.header().classes('items-center justify-between bg-zinc-900 shadow-lg').props("reveal reveal-offset=1"):
|
2026-02-25 15:26:24 +01:00
|
|
|
|
2026-03-01 20:34:42 +01:00
|
|
|
# --- LINKE SEITE ---
|
|
|
|
|
# Vereinslogo und den Titel in einer eigenen Reihe (Reihe 1)
|
2026-03-12 15:23:09 +01:00
|
|
|
with ui.row().classes('items-center'):
|
|
|
|
|
ui.image("gui/pictures/wsdg.png").classes('w-15 h-15 rounded-full')
|
2026-03-12 09:34:22 +01:00
|
|
|
ui.label('Diceghost Liga').classes('text-2xl font-bold text-normaltext')
|
2026-03-01 20:34:42 +01:00
|
|
|
|
2026-03-09 13:05:11 +01:00
|
|
|
# --- MITTE ---
|
2026-03-12 15:23:09 +01:00
|
|
|
if app.storage.user.get('authenticated', False):
|
|
|
|
|
discord_id = app.storage.user.get("discord_id")
|
|
|
|
|
if discord_id == admin_discord_id:
|
|
|
|
|
ui.button(icon="hardware", on_click=lambda: ui.navigate.to('/admin')).props("round")
|
2026-03-06 21:20:48 +01:00
|
|
|
|
2026-03-02 16:17:50 +01:00
|
|
|
# --- RECHTE SEITE ---
|
2026-02-27 12:01:02 +01:00
|
|
|
if app.storage.user.get('authenticated', False):
|
|
|
|
|
with ui.row().classes('items-center gap-4'):
|
2026-03-09 13:05:11 +01:00
|
|
|
ui.image(app.storage.user.get('discord_avatar_url')).classes('w-15 h-15 rounded-full')
|
2026-03-01 20:34:42 +01:00
|
|
|
discord_name = app.storage.user.get('discord_name')
|
|
|
|
|
display_name = app.storage.user.get('display_name')
|
2026-03-02 16:17:50 +01:00
|
|
|
player_id = app.storage.user.get('db_id')
|
2026-03-01 20:34:42 +01:00
|
|
|
|
2026-03-03 15:49:40 +01:00
|
|
|
# 1. kleine Funktion, die zwischen Text und Eingabe hin- und herschaltet
|
2026-03-01 20:34:42 +01:00
|
|
|
def toggle_edit_mode():
|
|
|
|
|
display_row.visible = not display_row.visible
|
|
|
|
|
edit_row.visible = not edit_row.visible
|
2026-03-12 15:23:09 +01:00
|
|
|
edit_button.visible = not edit_button.visible
|
2026-03-01 20:34:42 +01:00
|
|
|
|
|
|
|
|
# --- ANSICHT 1: Der normale Text mit Edit-Button ---
|
2026-03-12 15:23:09 +01:00
|
|
|
with ui.column().classes('items-center gap-0') as display_row:
|
|
|
|
|
with ui.column():
|
|
|
|
|
ui.label(display_name).classes('text-xl font-bold text-normaltext')
|
|
|
|
|
with ui.row().classes("items-center justify-between"):
|
|
|
|
|
ui.label("'aka'").classes('text-sm text-italic text-infotext')
|
|
|
|
|
ui.label(discord_name).classes('text-m text-bold text-infotext')
|
|
|
|
|
edit_button = ui.button(icon='edit', color='accent', on_click=toggle_edit_mode).props('round dense')
|
2026-03-01 20:34:42 +01:00
|
|
|
|
|
|
|
|
# --- ANSICHT 2: Das Eingabefeld (startet unsichtbar!) ---
|
2026-03-08 16:23:39 +01:00
|
|
|
with ui.row().classes('items-center gap-5') as edit_row:
|
2026-03-01 20:34:42 +01:00
|
|
|
edit_row.visible = False # Am Anfang verstecken
|
|
|
|
|
def save_new_name():
|
|
|
|
|
new_name = name_input.value
|
|
|
|
|
# Nur speichern, wenn ein Name drinsteht und er anders ist als vorher
|
|
|
|
|
if new_name and new_name != display_name:
|
2026-03-04 09:38:28 +01:00
|
|
|
print("save")
|
|
|
|
|
data_api.change_display_name(player_id, new_name) # Deine DB Funktion
|
2026-03-01 20:34:42 +01:00
|
|
|
app.storage.user['display_name'] = new_name
|
|
|
|
|
ui.notify('Name gespeichert!', color='positive')
|
|
|
|
|
ui.navigate.reload()
|
|
|
|
|
else:
|
|
|
|
|
# Wenn nichts geändert wurde, einfach wieder einklappen
|
|
|
|
|
toggle_edit_mode()
|
|
|
|
|
|
2026-03-08 16:23:39 +01:00
|
|
|
def generate_random_silly_name():
|
|
|
|
|
silly_name = data_api.generate_silly_name()
|
|
|
|
|
name_input.value=silly_name
|
|
|
|
|
|
2026-03-01 20:34:42 +01:00
|
|
|
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')
|
2026-03-12 09:34:22 +01:00
|
|
|
ui.button(icon='casino', color="accent", on_click=generate_random_silly_name).props('round dense')
|
2026-03-01 20:34:42 +01:00
|
|
|
ui.button(icon='close', color='negative', on_click=toggle_edit_mode).props('round dense')
|
|
|
|
|
|
2026-02-27 12:01:02 +01:00
|
|
|
avatar = app.storage.user.get('avatar_url')
|
|
|
|
|
if avatar:
|
2026-03-12 15:23:09 +01:00
|
|
|
ui.image(avatar).classes('w-5 h-5 rounded-full border-2 border-red-500')
|
2026-03-01 20:34:42 +01:00
|
|
|
|
|
|
|
|
def logout():
|
|
|
|
|
app.storage.user.clear()
|
|
|
|
|
ui.navigate.to('/')
|
2026-02-25 15:26:24 +01:00
|
|
|
|
2026-03-12 15:23:09 +01:00
|
|
|
ui.button(icon="logout", on_click=logout).props('round dense size=lg')
|
2026-02-25 15:26:24 +01:00
|
|
|
|
|
|
|
|
else:
|
2026-02-27 12:01:02 +01:00
|
|
|
auth_url = discord_login.get_auth_url()
|
|
|
|
|
ui.button('Login with Discord', on_click=lambda: ui.navigate.to(auth_url))
|
2026-03-01 20:34:42 +01:00
|
|
|
|
2026-02-27 12:01:02 +01:00
|
|
|
|
2026-03-10 15:34:25 +01:00
|
|
|
# ---------------------------
|
|
|
|
|
# --- Match Bestätigung ---
|
|
|
|
|
# ---------------------------
|
|
|
|
|
# Bestätigungs für offene Spiele --- Der "Marian Balken !!!1!11!"
|
2026-03-06 21:20:48 +01:00
|
|
|
if app.storage.user.get('authenticated', False):
|
|
|
|
|
unconfirmed_matches = data_api.get_unconfirmed_matches(player_id)
|
|
|
|
|
|
|
|
|
|
if len(unconfirmed_matches) > 0:
|
|
|
|
|
# Eine auffällige, rote Karte über die volle Breite
|
|
|
|
|
with ui.card().classes('w-full bg-red-900/80 border-2 border-red-500 mb-6'):
|
2026-03-12 15:23:09 +01:00
|
|
|
ui.label(f"Aktion erforderlich: Du hast {len(unconfirmed_matches)} offen(e) Spiel(e)!").classes('text-2xl font-bold text-normaltext mb-2')
|
2026-03-06 21:20:48 +01:00
|
|
|
|
|
|
|
|
for match in unconfirmed_matches:
|
|
|
|
|
|
2026-03-10 15:34:25 +01:00
|
|
|
# Button Funktionen. Akzeptieren oder Rejecten. Der Reject Button löscht das Match aus der DB.
|
2026-03-06 21:20:48 +01:00
|
|
|
def reject_match(m_id):
|
2026-03-13 11:29:01 +01:00
|
|
|
data_api.delete_match(m_id, player_id)
|
2026-03-06 21:20:48 +01:00
|
|
|
ui.notify("Spiel abgelehnt und gelöscht!", color="warning")
|
|
|
|
|
ui.navigate.reload() # Lädt die Seite neu, um die Karte zu aktualisieren
|
|
|
|
|
|
|
|
|
|
def acccept_match(m_id):
|
|
|
|
|
ui.notify("Spiel akzeptiert. Wird Berechnet.")
|
|
|
|
|
ui.navigate.reload() # Lädt die Seite neu, um die Karte zu aktualisieren
|
2026-03-16 16:04:08 +01:00
|
|
|
data_api.confirm_match(m_id)
|
2026-03-06 21:20:48 +01:00
|
|
|
calc_match.calculate_match(m_id)
|
|
|
|
|
|
|
|
|
|
with ui.row().classes('w-full items-center justify-between bg-zinc-900 p-3 rounded shadow-inner'):
|
2026-03-18 16:14:29 +01:00
|
|
|
info_text = f"{match['system_name']} - {match["played_at"]} "
|
|
|
|
|
detail_text = f"{match['p1_name']} behauptet: {match['p1_name']} ({match['score_player1']}) vs. Du ({match['score_player2']})"
|
|
|
|
|
ui.label(info_text).classes('text-bold text-lg text-normaltext')
|
|
|
|
|
ui.label(detail_text).classes('text-bold text-normaltext')
|
2026-03-06 21:20:48 +01:00
|
|
|
|
|
|
|
|
# Die Buttons (Funktion machen wir im nächsten Schritt!)
|
|
|
|
|
with ui.row().classes('gap-2'):
|
|
|
|
|
# ABLEHNEN und Spiel löschen
|
2026-03-12 15:23:09 +01:00
|
|
|
ui.button(color="negative", icon="close", on_click=lambda e, m_id=match['match_id']: reject_match(m_id))
|
2026-03-06 21:20:48 +01:00
|
|
|
ui.space()
|
|
|
|
|
# BESTÄTIGEN und spiel berechnen lassen
|
2026-03-12 15:23:09 +01:00
|
|
|
ui.button(color="positive", icon="check", on_click=lambda e, m_id=match['match_id']: acccept_match(m_id))
|
2026-03-18 16:14:29 +01:00
|
|
|
ui.label("Bestätigen wenn die Angaben stimmen, ablehnen wenn sich ein Fehler eingeschlichen hat.")
|
2026-03-10 15:34:25 +01:00
|
|
|
|
2026-03-08 16:30:52 +01:00
|
|
|
# ---------------------------
|
|
|
|
|
# --- Selbst eingetragene, offene Spiele ---
|
|
|
|
|
# ---------------------------
|
|
|
|
|
submitted_matches = data_api.get_submitted_matches(player_id)
|
|
|
|
|
if len(submitted_matches) > 0:
|
|
|
|
|
# Eine etwas dezentere Karte (grau)
|
|
|
|
|
with ui.card().classes('w-full bg-zinc-800 border border-gray-600 mb-6'):
|
2026-03-18 16:14:29 +01:00
|
|
|
ui.label(f"Warten auf Gegner: Du hast {len(submitted_matches)} offene(s) Spiel(e).").classes('text-xl font-bold text-gray-300 mb-2')
|
2026-03-08 16:30:52 +01:00
|
|
|
|
|
|
|
|
for match in submitted_matches:
|
|
|
|
|
# Die Lösch-Funktion, die beim Klick ausgeführt wird
|
|
|
|
|
def retract_match(m_id):
|
2026-03-10 15:34:25 +01:00
|
|
|
data_api.delete_match(m_id, player_id)
|
2026-03-08 16:30:52 +01:00
|
|
|
ui.notify("Eingetragenes Spiel zurückgezogen!", color="warning")
|
|
|
|
|
ui.navigate.reload()
|
|
|
|
|
|
|
|
|
|
# Für jedes Match machen wir eine kleine Reihe
|
|
|
|
|
with ui.row().classes('w-full items-center justify-between bg-zinc-900 p-3 rounded shadow-inner'):
|
|
|
|
|
# Info-Text: Auf wen warten wir?
|
2026-03-18 16:14:29 +01:00
|
|
|
info_text = f"{match['system_name']} - {match["played_at"]} "
|
|
|
|
|
detail_text = f"Du ({match["score_player1"]}) vs. {match["p2_name"]}({match["score_player2"]})"
|
|
|
|
|
ui.label(info_text).classes('text-bold text-lg text-normaltext')
|
|
|
|
|
ui.label(detail_text).classes('text-bold text-normaltext')
|
|
|
|
|
ui.button(color="warning", icon="delete", on_click=lambda e, m_id=match['match_id']: retract_match(m_id))
|
|
|
|
|
ui.label("Dein Gegner muss das Match noch bestätigen. Wenn du einen Fehler gemacht hast, kannst du es löschen.")
|
2026-03-06 21:20:48 +01:00
|
|
|
|
2026-03-03 15:49:40 +01:00
|
|
|
# ---------------------------
|
2026-02-27 12:01:02 +01:00
|
|
|
# --- Spielsysteme ---
|
2026-03-03 15:49:40 +01:00
|
|
|
# ---------------------------
|
2026-02-26 16:20:05 +01:00
|
|
|
if app.storage.user.get('authenticated', False):
|
2026-03-12 15:23:09 +01:00
|
|
|
with ui.card().classes("w-full items-center"):
|
|
|
|
|
with ui.row():
|
|
|
|
|
ui.label(text="Meine Ligaplätze").classes("font-bold text-white text-xl text-normaltext")
|
2026-03-12 09:34:22 +01:00
|
|
|
info_system.create_info_button("league_info")
|
2026-03-02 16:17:50 +01:00
|
|
|
|
|
|
|
|
placements = data_api.get_player_statistics(player_id)
|
2026-03-17 16:08:38 +01:00
|
|
|
systems = data_api.get_gamesystems_data()
|
2026-03-09 08:57:39 +01:00
|
|
|
|
|
|
|
|
my_stats = { p['gamesystem_id']: p for p in placements }
|
|
|
|
|
|
2026-03-02 16:17:50 +01:00
|
|
|
def click_join_league(p_id, sys_id):
|
|
|
|
|
data_api.join_league(p_id, sys_id)
|
|
|
|
|
ui.navigate.to("/")
|
|
|
|
|
|
2026-03-03 15:49:40 +01:00
|
|
|
def toggle_visibility(row_a, row_b):
|
|
|
|
|
row_a.visible = not row_a.visible
|
|
|
|
|
row_b.visible = not row_b.visible
|
|
|
|
|
|
2026-03-04 16:08:45 +01:00
|
|
|
with ui.element('div').classes("w-full grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 items-center"):
|
2026-03-09 08:57:39 +01:00
|
|
|
|
|
|
|
|
for sys in systems:
|
|
|
|
|
sys_id = sys['id']
|
|
|
|
|
sys_name = sys['name']
|
2026-03-10 15:34:25 +01:00
|
|
|
sys_logo = sys["picture"]
|
|
|
|
|
sys_description = sys['description']
|
2026-03-09 08:57:39 +01:00
|
|
|
|
2026-03-02 16:17:50 +01:00
|
|
|
sys_card = ui.card().classes("h-60 w-full items-center justify-center transition-colors")
|
2026-03-10 15:34:25 +01:00
|
|
|
|
2026-03-02 16:17:50 +01:00
|
|
|
with sys_card:
|
2026-03-12 15:23:09 +01:00
|
|
|
ui.label(text=sys_name).classes('text-xl font-bold text-center text-normaltext')
|
2026-03-10 15:34:25 +01:00
|
|
|
if sys_logo:
|
|
|
|
|
ui.image(f"/pictures/{sys_logo}").classes("w-60")
|
2026-03-02 16:17:50 +01:00
|
|
|
|
2026-03-10 15:34:25 +01:00
|
|
|
if sys_description:
|
2026-03-12 15:23:09 +01:00
|
|
|
ui.label(text=sys['description']).classes('text-xs text-gray-400 text-center mt-2 text-infotext')
|
2026-03-02 16:17:50 +01:00
|
|
|
|
2026-03-09 08:57:39 +01:00
|
|
|
# Prüfen: Ist diese sys_id in den Stats des Spielers? UND hat er ein MMR?
|
|
|
|
|
if sys_id not in my_stats or my_stats[sys_id]['mmr'] is None:
|
2026-03-02 16:17:50 +01:00
|
|
|
ui.label(text="Du bist noch nicht in dieser Liga.").classes("text-red-500 font-bold")
|
|
|
|
|
|
2026-03-03 15:49:40 +01:00
|
|
|
join_row = ui.row().classes('items-center gap-2')
|
|
|
|
|
confirm_row = ui.row().classes('items-center gap-2')
|
2026-03-09 08:57:39 +01:00
|
|
|
confirm_row.visible = False
|
2026-03-03 15:49:40 +01:00
|
|
|
|
|
|
|
|
with join_row:
|
|
|
|
|
ui.button("Beitreten", on_click=lambda e, r1=join_row, r2=confirm_row: toggle_visibility(r1, r2))
|
2026-03-02 16:17:50 +01:00
|
|
|
|
2026-03-03 15:49:40 +01:00
|
|
|
with confirm_row:
|
2026-03-09 08:57:39 +01:00
|
|
|
ui.button("Liga Beitreten", color="green", on_click=lambda e, p=player_id, s=sys_id: click_join_league(p, s))
|
2026-03-03 15:49:40 +01:00
|
|
|
ui.button(icon='cancel', color='red', on_click=lambda e, r1=join_row, r2=confirm_row: toggle_visibility(r1, r2)).props('round dense')
|
2026-03-02 16:17:50 +01:00
|
|
|
else:
|
|
|
|
|
# Spieler IST in der Liga!
|
2026-03-09 08:57:39 +01:00
|
|
|
stat = my_stats[sys_id] # Wir holen uns seine Stats aus dem Wörterbuch
|
|
|
|
|
|
2026-03-02 16:17:50 +01:00
|
|
|
sys_card.classes("cursor-pointer hover:bg-zinc-800")
|
2026-03-09 08:57:39 +01:00
|
|
|
sys_card.on('click', lambda e, name=sys_name: ui.navigate.to(f'/statistic/{name}'))
|
2026-03-02 16:17:50 +01:00
|
|
|
|
|
|
|
|
with ui.row().classes('items-center gap-4'):
|
2026-03-12 15:23:09 +01:00
|
|
|
ui.label(text=f"MMR: {stat['mmr']}").classes("text-lg font-bold text-accenttext")
|
|
|
|
|
ui.label(text=f"Spiele: {stat['games_in_system']}").classes("text-lg font-bold text-accenttext")
|
2026-03-09 08:57:39 +01:00
|
|
|
|
|
|
|
|
|
2026-03-03 15:49:40 +01:00
|
|
|
|
|
|
|
|
# ---------------------------
|
|
|
|
|
# Match Historie
|
|
|
|
|
# ---------------------------
|
2026-03-12 15:23:09 +01:00
|
|
|
with ui.card().classes("w-full items-center"):
|
|
|
|
|
ui.label(text= "Meine letzten Spiele").classes("font-bold text-normaltext text-xl")
|
2026-03-08 20:56:54 +01:00
|
|
|
# 1. Daten aus der DB holen ABER per [:5] hart auf die neuesten 5 Listen-Einträge abschneiden!
|
|
|
|
|
raw_matches = data_api.get_recent_matches_for_player(player_id)[:5]
|
2026-03-03 15:49:40 +01:00
|
|
|
|
2026-03-08 20:56:54 +01:00
|
|
|
# 2. Daten für die Tabelle aufbereiten (Bleibt exakt gleich wie bei dir)
|
2026-03-03 15:49:40 +01:00
|
|
|
table_rows = []
|
|
|
|
|
for match in raw_matches:
|
|
|
|
|
if match['player1_id'] == player_id:
|
|
|
|
|
opponent_name = f"{match['p2_display']} aka {match['p2_discord']}"
|
|
|
|
|
my_score = match['score_player1']
|
|
|
|
|
opp_score = match['score_player2']
|
|
|
|
|
else:
|
|
|
|
|
opponent_name = f"{match['p1_display']} aka {match['p1_discord']}"
|
|
|
|
|
my_score = match['score_player2']
|
|
|
|
|
opp_score = match['score_player1']
|
|
|
|
|
|
|
|
|
|
if my_score > opp_score:
|
|
|
|
|
result_text = "Gewonnen"
|
|
|
|
|
elif my_score < opp_score:
|
|
|
|
|
result_text = "Verloren"
|
|
|
|
|
else:
|
|
|
|
|
result_text = "Unentschieden"
|
|
|
|
|
|
|
|
|
|
date_clean = str(match['played_at'])[:10]
|
|
|
|
|
|
|
|
|
|
table_rows.append({
|
|
|
|
|
'date': date_clean,
|
|
|
|
|
'system': match['gamesystem_name'],
|
|
|
|
|
'opponent': opponent_name,
|
|
|
|
|
'result': f"{result_text} ({my_score} : {opp_score})"
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
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'}
|
|
|
|
|
]
|
|
|
|
|
|
2026-03-08 20:56:54 +01:00
|
|
|
# 4. Die Tabelle zeichnen und den NEUEN BUTTON hinzufügen
|
2026-03-03 15:49:40 +01:00
|
|
|
if len(table_rows) > 0:
|
|
|
|
|
ui.table(columns=table_columns, rows=table_rows, row_key='date').classes('w-full bg-zinc-900 text-white')
|
2026-03-08 20:56:54 +01:00
|
|
|
|
|
|
|
|
# NEU: Der Button, der zur großen Log-Seite führt
|
2026-03-11 19:36:11 +01:00
|
|
|
ui.button("Komplette Historie & Log anzeigen", icon="history", on_click=lambda: ui.navigate.to('/matchhistory')).classes('mt-4 bg-zinc-700 text-white hover:bg-zinc-600')
|
2026-03-03 15:49:40 +01:00
|
|
|
else:
|
2026-03-12 15:23:09 +01:00
|
|
|
ui.label("Noch keine Spiele absolviert.").classes("text-infotext italic")
|
2026-03-03 15:49:40 +01:00
|
|
|
|