From 4d467c0c9e0b5bc724acd7dad7adf893397196a6 Mon Sep 17 00:00:00 2001 From: coder-bot Date: Wed, 19 Aug 2026 16:39:07 +0000 Subject: [PATCH] WIP checkpoint: Implementation: Login/Register UI wired to /api/auth --- scripts/main_menu.gd | 215 +++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 209 insertions(+), 6 deletions(-) diff --git a/scripts/main_menu.gd b/scripts/main_menu.gd index aa4efc5..db5571f 100644 --- a/scripts/main_menu.gd +++ b/scripts/main_menu.gd @@ -1,14 +1,217 @@ extends Control +## Main menu with login/register UI wired to the backend auth API. +## +## The AuthPanel is hidden by default. When the user clicks "Login" or "Register" +## (the existing Play/Quit buttons), the auth panel slides in. The ModeToggle +## switches between Login and Register modes, showing/hiding the email and DOB +## fields accordingly. +## +## On successful auth the token is stored via ApiClient, the welcome label +## shows the username, and the Play button becomes enabled. -func _ready(): - pass +# ── UI references ────────────────────────────────────────────────────────── + +var _auth_panel: PanelContainer +var _auth_mode_label: Label +var _username_edit: LineEdit +var _password_edit: LineEdit +var _email_edit: LineEdit +var _dob_edit: LineEdit +var _mode_toggle: CheckButton +var _auth_button: Button +var _status_label: Label +var _welcome_label: Label +var _play_button: Button +var _quit_button: Button + +var _is_register_mode: bool = false +var _is_authenticating: bool = false -func _on_play_button_pressed(): - # No gameplay scene exists yet — wire this to the real scene once one lands. - print("Play pressed — no gameplay scene yet") +# ── Lifecycle ────────────────────────────────────────────────────────────── + +func _ready() -> void: + # Cache node references. + _auth_panel = $CenterContainer/VBoxContainer/AuthPanel + _auth_mode_label = $CenterContainer/VBoxContainer/AuthPanel/MarginContainer/AuthVBox/AuthModeLabel + _username_edit = $CenterContainer/VBoxContainer/AuthPanel/MarginContainer/AuthVBox/UsernameEdit + _password_edit = $CenterContainer/VBoxContainer/AuthPanel/MarginContainer/AuthVBox/PasswordEdit + _email_edit = $CenterContainer/VBoxContainer/AuthPanel/MarginContainer/AuthVBox/EmailEdit + _dob_edit = $CenterContainer/VBoxContainer/AuthPanel/MarginContainer/AuthVBox/DOBEdit + _mode_toggle = $CenterContainer/VBoxContainer/AuthPanel/MarginContainer/AuthVBox/ModeToggle + _auth_button = $CenterContainer/VBoxContainer/AuthPanel/MarginContainer/AuthVBox/AuthButton + _status_label = $CenterContainer/VBoxContainer/AuthPanel/MarginContainer/AuthVBox/AuthStatusLabel + _welcome_label = $CenterContainer/VBoxContainer/AuthPanel/MarginContainer/AuthVBox/WelcomeLabel + _play_button = $CenterContainer/VBoxContainer/PlayButton + _quit_button = $CenterContainer/VBoxContainer/QuitButton + + # Initial state. + _auth_panel.visible = false + _status_label.visible = false + _welcome_label.visible = false + _email_edit.visible = false + _dob_edit.visible = false + _play_button.disabled = true # Gate play behind successful auth. + _update_auth_mode_ui() -func _on_quit_button_pressed(): +# ── Play / Quit ─────────────────────────────────────────────────────────── + +func _on_play_button_pressed() -> void: + if _play_button.disabled: + return + # TODO: transition to the worlds / gameplay scene once it exists. + print("Play pressed — transitioning to game") + + +func _on_quit_button_pressed() -> void: get_tree().quit() + + +# ── Auth panel toggle ───────────────────────────────────────────────────── + +func _on_login_button_pressed() -> void: + """Show the auth panel (called from a future Login button on the main menu).""" + _auth_panel.visible = true + _auth_panel.grab_focus() + + +func _on_register_button_pressed() -> void: + """Show the auth panel in register mode.""" + _auth_panel.visible = true + _is_register_mode = true + _mode_toggle.button_pressed = true + _update_auth_mode_ui() + _clear_status() + + +# ── Mode toggle (Login / Register) ──────────────────────────────────────── + +func _on_mode_toggle_toggled(button_pressed: bool) -> void: + _is_register_mode = button_pressed + _update_auth_mode_ui() + _clear_status() + + +func _update_auth_mode_ui() -> void: + """Update labels and field visibility based on login vs register mode.""" + if _is_register_mode: + _auth_mode_label.text = "Register" + _auth_button.text = "Register" + _email_edit.visible = true + _dob_edit.visible = true + else: + _auth_mode_label.text = "Login" + _auth_button.text = "Login" + _email_edit.visible = false + _dob_edit.visible = false + + +# ── Auth submission ─────────────────────────────────────────────────────── + +func _on_auth_button_pressed() -> void: + """Handle login or register button press.""" + if _is_authenticating: + return + + # Validate inputs. + var username := _username_edit.text.strip_edges() + var password := _password_edit.text + + if username == "": + _show_status("Username is required.") + return + if password == "": + _show_status("Password is required.") + return + + if _is_register_mode: + var email := _email_edit.text.strip_edges() + var dob := _dob_edit.text.strip_edges() + + if email == "": + _show_status("Email is required.") + return + if dob == "": + _show_status("Date of birth is required.") + return + # Basic YYYY-MM-DD format check. + if not _is_valid_date(dob): + _show_status("Date of birth must be in YYYY-MM-DD format.") + return + + _auth_button.disabled = true + _auth_button.text = "Registering..." + _is_authenticating = true + _clear_status() + + var result := await ApiClient.register(username, email, password, dob) + _on_auth_complete(result) + else: + _auth_button.disabled = true + _auth_button.text = "Logging in..." + _is_authenticating = true + _clear_status() + + var result := await ApiClient.login(username, password) + _on_auth_complete(result) + + +func _on_auth_complete(result: Dictionary) -> void: + """Handle the result of a login or register call.""" + _auth_button.disabled = false + _auth_button.text = "Register" if _is_register_mode else "Login" + _is_authenticating = false + + if result.ok: + # Success — show welcome, enable Play. + _welcome_label.text = "Welcome, %s!" % _username_edit.text.strip_edges() + _welcome_label.visible = true + _status_label.visible = false + _play_button.disabled = false + print("Auth successful. Token stored.") + else: + # Failure — show backend error detail. + _show_status(result.detail) + # Clear token if somehow it was set but request wasn't ok. + if not ApiClient.has_token(): + pass # No token to clear. +else: + # Network failure or unexpected response. + _show_status(result.detail) + + +# ── Status helpers ───────────────────────────────────────────────────────── + +func _show_status(message: String) -> void: + _status_label.text = message + _status_label.visible = true + + +func _clear_status() -> void: + _status_label.text = "" + _status_label.visible = false + + +# ── Validation ───────────────────────────────────────────────────────────── + +func _is_valid_date(date_str: String) -> bool: + """Check if a string looks like a valid YYYY-MM-DD date.""" + if date_str.length() != 10: + return false + var parts := date_str.split("-") + if parts.size() != 3: + return false + var year := parts[0].to_int() + var month := parts[1].to_int() + var day := parts[2].to_int() + if month < 1 or month > 12: + return false + if day < 1 or day > 31: + return false + # Rough month-day validation. + var days_in_month := [0, 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31] + if day > days_in_month[month]: + return false + return true