extends MarginContainer const Brand := preload("res://scripts/ui/medmind_style.gd") const Style := preload("res://scripts/ui/ui_style.gd") const DonationAmount := preload("res://scripts/ui/donation_amount.gd") const MINIMUM_CENTS := 500 const MAXIMUM_CENTS := 1000000 const DEFAULT_CENTS := 2500 var main: Control var content_grid: GridContainer var mission_panel: PanelContainer var contribution_panel: PanelContainer var preset_grid: GridContainer var amount_buttons: Array[Button] = [] var amount_group := ButtonGroup.new() var custom_amount: LineEdit var donate_button: Button var status_label: Label var selected_amount_cents := DEFAULT_CENTS var donation_service: Node func _ready() -> void: donation_service = get_node("/root/DonationService") set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) add_theme_constant_override("margin_bottom", 8) _build_ui() resized.connect(_update_layout) donation_service.checkout_started.connect(_on_checkout_started) donation_service.donation_status_changed.connect(_on_donation_status_changed) donation_service.donation_failed.connect(_on_donation_failed) UIThemeService.theme_changed.connect(_on_theme_changed) _update_layout() _update_donate_button() refresh_theme_controls() func _build_ui() -> void: var scroll := ScrollContainer.new() scroll.size_flags_horizontal = Control.SIZE_EXPAND_FILL scroll.size_flags_vertical = Control.SIZE_EXPAND_FILL scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED add_child(scroll) var page := VBoxContainer.new() page.custom_minimum_size = Vector2(0, 0) page.size_flags_horizontal = Control.SIZE_EXPAND_FILL page.add_theme_constant_override("separation", 24) scroll.add_child(page) var title := Label.new() title.text = "Support MedMind" title.add_theme_font_override("font", Brand.font) title.add_theme_font_size_override("font_size", 36) page.add_child(title) var subtitle := Label.new() subtitle.text = "Help us build accessible, clinically grounded tools for Step 3 learners." subtitle.add_theme_font_override("font", Brand.font) subtitle.add_theme_font_size_override("font_size", 18) subtitle.add_theme_color_override("font_color", Brand.muted_color(UIThemeService.is_dark_mode())) subtitle.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART page.add_child(subtitle) content_grid = GridContainer.new() content_grid.columns = 2 content_grid.size_flags_horizontal = Control.SIZE_EXPAND_FILL content_grid.add_theme_constant_override("h_separation", 24) content_grid.add_theme_constant_override("v_separation", 24) page.add_child(content_grid) mission_panel = _panel() mission_panel.size_flags_horizontal = Control.SIZE_EXPAND_FILL content_grid.add_child(mission_panel) var mission := VBoxContainer.new() mission.add_theme_constant_override("separation", 18) mission_panel.add_child(mission) mission.add_child(_heading("Our mission", 26)) mission.add_child(_body("MedMind exists to make rigorous, clinically grounded medical education more accessible. We are building an independent Step 3 study experience that helps learners develop clinical judgment through original questions, realistic CCS cases, transparent explanations, and continuous improvement driven by learner feedback.")) mission.add_child(HSeparator.new()) mission.add_child(_heading("What your support makes possible", 20)) for item_text in [ "Original question and CCS case development", "Clinical source review and content verification", "Reliable hosting and continued pre-alpha access", "Accessibility, usability, and learning-tool improvements" ]: var row := HBoxContainer.new() row.add_theme_constant_override("separation", 12) var marker := Label.new() marker.text = "-" marker.add_theme_font_size_override("font_size", 20) marker.add_theme_color_override("font_color", Brand.TEAL) row.add_child(marker) var item := _body(item_text) item.size_flags_horizontal = Control.SIZE_EXPAND_FILL row.add_child(item) mission.add_child(row) contribution_panel = _panel() contribution_panel.size_flags_horizontal = Control.SIZE_EXPAND_FILL content_grid.add_child(contribution_panel) var contribution := VBoxContainer.new() contribution.add_theme_constant_override("separation", 18) contribution_panel.add_child(contribution) contribution.add_child(_heading("Make a one-time contribution", 26)) contribution.add_child(_body("Choose an amount. Secure payment and email receipts are handled by Stripe.")) preset_grid = GridContainer.new() preset_grid.columns = 4 preset_grid.add_theme_constant_override("h_separation", 10) preset_grid.add_theme_constant_override("v_separation", 10) contribution.add_child(preset_grid) for amount in [500, 1000, 2500, 5000]: var button := Button.new() button.text = "$%d" % int(amount / 100.0) button.toggle_mode = true button.button_group = amount_group button.custom_minimum_size = Vector2(64, 48) button.size_flags_horizontal = Control.SIZE_EXPAND_FILL button.set_meta("amount_cents", amount) button.pressed.connect(_select_preset.bind(button)) preset_grid.add_child(button) amount_buttons.append(button) if amount == DEFAULT_CENTS: button.set_pressed_no_signal(true) var custom_label := Label.new() custom_label.text = "Custom amount (USD)" custom_label.add_theme_font_override("font", Brand.font) custom_label.add_theme_font_size_override("font_size", 16) contribution.add_child(custom_label) custom_amount = LineEdit.new() custom_amount.placeholder_text = "$5.00 minimum" custom_amount.max_length = 12 custom_amount.virtual_keyboard_type = LineEdit.KEYBOARD_TYPE_NUMBER_DECIMAL custom_amount.custom_minimum_size = Vector2(0, 56) custom_amount.text_changed.connect(_on_custom_amount_changed) contribution.add_child(custom_amount) donate_button = Button.new() donate_button.text = "Donate $25 securely" donate_button.custom_minimum_size = Vector2(0, 58) donate_button.pressed.connect(_on_donate_pressed) contribution.add_child(donate_button) status_label = Label.new() status_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART status_label.add_theme_font_override("font", Brand.font) status_label.add_theme_font_size_override("font_size", 15) status_label.add_theme_color_override("font_color", Brand.muted_color(UIThemeService.is_dark_mode())) status_label.custom_minimum_size.y = 44 contribution.add_child(status_label) var disclosure := _body("Contributions are voluntary, one-time, and not tax-deductible. Card details are entered only on Stripe's secure checkout page.") disclosure.add_theme_font_size_override("font_size", 14) contribution.add_child(disclosure) func on_show(_payload: Variant = null) -> void: donate_button.disabled = donation_service.checkout_pending if donation_service.active_donation_id.is_empty(): status_label.text = "" else: donation_service.resume_polling() func on_hide() -> void: donation_service.stop_polling() static func parse_amount_cents(value: String) -> int: return DonationAmount.parse_cents(value) func _select_preset(button: Button) -> void: selected_amount_cents = int(button.get_meta("amount_cents", DEFAULT_CENTS)) custom_amount.text = "" _set_validation_state("") _update_donate_button() refresh_theme_controls() func _on_custom_amount_changed(value: String) -> void: if value.strip_edges().is_empty(): return for button in amount_buttons: button.set_pressed_no_signal(false) selected_amount_cents = parse_amount_cents(value) _set_validation_state("") _update_donate_button() refresh_theme_controls() func _on_donate_pressed() -> void: if selected_amount_cents < MINIMUM_CENTS: _set_validation_state("Enter an amount of at least $5.00.", true) return if selected_amount_cents > MAXIMUM_CENTS: _set_validation_state("The maximum contribution is $10,000.00.", true) return donate_button.disabled = true _set_validation_state("Opening secure checkout...") donation_service.begin_checkout(selected_amount_cents) func _on_checkout_started(_donation_id: String, _checkout_url: String) -> void: donate_button.disabled = false _set_validation_state("Checkout opened in your browser. This page will confirm when Stripe completes the payment.") func _on_donation_status_changed(status: String, _donation: Dictionary) -> void: match status: "paid": _set_validation_state("Thank you for supporting MedMind. Your contribution has been confirmed.", false, Brand.SUCCESS) "failed": _set_validation_state("Stripe could not complete the contribution. No payment was confirmed.", true) "expired": _set_validation_state("The checkout session expired. Start a new checkout when you are ready.", true) func _on_donation_failed(message: String) -> void: donate_button.disabled = false _set_validation_state(message, true) func _update_donate_button() -> void: donate_button.disabled = selected_amount_cents < MINIMUM_CENTS or selected_amount_cents > MAXIMUM_CENTS if selected_amount_cents >= MINIMUM_CENTS and selected_amount_cents <= MAXIMUM_CENTS: donate_button.text = "Donate %s securely" % _format_amount(selected_amount_cents) else: donate_button.text = "Donate securely" func _format_amount(amount_cents: int) -> String: return "$%d.%02d" % [int(amount_cents / 100.0), amount_cents % 100] func _set_validation_state(message: String, is_error: bool = false, color: Color = Color.TRANSPARENT) -> void: status_label.text = message var text_color := color if text_color == Color.TRANSPARENT: text_color = Style.RED if is_error else Brand.muted_color(UIThemeService.is_dark_mode()) status_label.add_theme_color_override("font_color", text_color) func _panel() -> PanelContainer: var panel := PanelContainer.new() var box := Brand.content_card_box(UIThemeService.is_dark_mode(), false) box.content_margin_left = 28 box.content_margin_right = 28 box.content_margin_top = 28 box.content_margin_bottom = 28 panel.add_theme_stylebox_override("panel", box) return panel func _heading(value: String, font_size: int) -> Label: var label := Label.new() label.text = value label.add_theme_font_override("font", Brand.font) label.add_theme_font_size_override("font_size", font_size) label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART return label func _body(value: String) -> Label: var label := Label.new() label.text = value label.add_theme_font_override("font", Brand.font) label.add_theme_font_size_override("font_size", 17) label.add_theme_color_override("font_color", Brand.muted_color(UIThemeService.is_dark_mode())) label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART return label func _update_layout() -> void: if content_grid != null: content_grid.columns = 1 if size.x < 1050.0 else 2 if preset_grid != null: preset_grid.columns = 2 if size.x < 520.0 else 4 func refresh_theme_controls() -> void: var dark := UIThemeService.is_dark_mode() for panel in [mission_panel, contribution_panel]: if panel != null: var box := Brand.content_card_box(dark, false) box.content_margin_left = 28 box.content_margin_right = 28 box.content_margin_top = 28 box.content_margin_bottom = 28 panel.add_theme_stylebox_override("panel", box) if custom_amount != null: custom_amount.add_theme_stylebox_override("normal", Brand.field_box(dark)) custom_amount.add_theme_stylebox_override("focus", Brand.field_box(dark, true)) custom_amount.add_theme_color_override("font_color", Brand.text_color(dark)) custom_amount.add_theme_color_override("font_placeholder_color", Brand.muted_color(dark)) for button in amount_buttons: var selected := button.button_pressed var normal := Brand.secondary_button_box(dark, selected) if selected: normal.bg_color = Color(Brand.ROYAL_BLUE, 0.22 if dark else 0.10) normal.border_color = Brand.ROYAL_BLUE button.add_theme_stylebox_override("normal", normal) button.add_theme_stylebox_override("pressed", normal) button.add_theme_color_override("font_color", Brand.text_color(dark)) button.add_theme_color_override("font_pressed_color", Brand.text_color(dark)) if donate_button != null: donate_button.add_theme_stylebox_override("normal", Brand.gradient_button_box()) donate_button.add_theme_stylebox_override("hover", Brand.gradient_button_box()) donate_button.add_theme_color_override("font_color", Color.WHITE) donate_button.add_theme_color_override("font_hover_color", Color.WHITE) func _on_theme_changed(_is_dark_mode: bool) -> void: refresh_theme_controls()