Add tab row, feature bullets, and a Popular badge to the RZ Coin Shop

Matches the latest reference more closely: a Featured/Tokens/Boosts/
Content/Customization/Bundles tab row (only Featured is populated -
the rest are inert labels since there's no real data behind them
yet), per-card checkmark feature bullets, and a Popular badge on the
Exam Simulation Token card. Client-side presentation only, no backend
changes.
This commit is contained in:
hz
2026-07-29 20:52:59 -05:00
parent 19371b7a93
commit a162ce31c6
+200 -60
View File
@@ -5,6 +5,19 @@ const ICON_BY_SKU := {
"exam_token_1": "clock-3",
"qbank_reset_1": "rotate-ccw",
}
const FEATURES_BY_SKU := {
"exam_token_1": ["Full 6-block exam", "Timed & scored", "Performance analytics"],
"qbank_reset_1": ["Reset eligibility", "Keep your stats", "Smart spaced retention"],
}
const POPULAR_SKUS := ["exam_token_1"]
const TAB_DEFS := [
{"label": "Featured", "icon": "star", "active": true},
{"label": "Tokens", "icon": "clipboard-list", "active": false},
{"label": "Boosts", "icon": "activity", "active": false},
{"label": "Content", "icon": "book-open", "active": false},
{"label": "Customization", "icon": "settings", "active": false},
{"label": "Bundles", "icon": "clipboard-plus", "active": false},
]
var main: Control
var subtitle_label: Label
@@ -14,6 +27,9 @@ var summary_icon: TextureRect
var balance_value_label: Label
var coin_caption_label: Label
var inventory_label: Label
var tab_controls: Array = []
var section_heading: Label
var section_subtitle: Label
var products_grid: HFlowContainer
var status_label: Label
var confirm_dialog: ConfirmationDialog
@@ -32,12 +48,19 @@ func _ready() -> void:
_build_header()
_build_summary_card()
_build_tabs()
var section_title := Label.new()
section_title.text = "Featured"
section_title.add_theme_font_override("font", Brand.font)
section_title.add_theme_font_size_override("font_size", 18)
add_child(section_title)
section_heading = Label.new()
section_heading.text = "Featured"
section_heading.add_theme_font_override("font", Brand.font)
section_heading.add_theme_font_size_override("font_size", 18)
add_child(section_heading)
section_subtitle = Label.new()
section_subtitle.text = "Hand-picked items to supercharge your Step 3 prep."
section_subtitle.add_theme_font_override("font", Brand.font)
section_subtitle.add_theme_font_size_override("font_size", 12)
section_subtitle.add_theme_color_override("font_color", Brand.muted_color(UIThemeService.is_dark_mode()))
add_child(section_subtitle)
products_grid = HFlowContainer.new()
products_grid.add_theme_constant_override("h_separation", 14)
@@ -134,6 +157,66 @@ func _build_summary_card() -> void:
row.add_child(copy)
func _build_tabs() -> void:
var dark := UIThemeService.is_dark_mode()
tab_controls.clear()
var tabs_row := HFlowContainer.new()
tabs_row.add_theme_constant_override("h_separation", 20)
tabs_row.add_theme_constant_override("v_separation", 8)
add_child(tabs_row)
for tab_def_variant in TAB_DEFS:
var tab_def: Dictionary = tab_def_variant
var tab := VBoxContainer.new()
tab.add_theme_constant_override("separation", 6)
tabs_row.add_child(tab)
var content := HBoxContainer.new()
content.add_theme_constant_override("separation", 6)
tab.add_child(content)
var icon := TextureRect.new()
icon.texture = load("res://assets/icons/lucide/%s.svg" % str(tab_def.get("icon", "star")))
icon.custom_minimum_size = Vector2(16, 16)
icon.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
icon.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
content.add_child(icon)
var label := Label.new()
label.text = str(tab_def.get("label", ""))
label.add_theme_font_override("font", Brand.font)
label.add_theme_font_size_override("font_size", 14)
content.add_child(label)
var underline := ColorRect.new()
underline.custom_minimum_size = Vector2(0, 2)
tab.add_child(underline)
var active := bool(tab_def.get("active", false))
tab_controls.append({"icon": icon, "label": label, "underline": underline, "active": active})
var divider := ColorRect.new()
divider.custom_minimum_size = Vector2(0, 1)
add_child(divider)
tab_controls.append({"divider": divider})
_apply_tab_theme(dark)
func _apply_tab_theme(dark: bool) -> void:
for entry_variant in tab_controls:
var entry: Dictionary = entry_variant
if entry.has("divider"):
(entry["divider"] as ColorRect).color = Color("2a365a") if dark else Brand.LIGHT_BORDER
continue
var active := bool(entry["active"])
var color := Brand.ROYAL_BLUE if active else Brand.muted_color(dark)
(entry["icon"] as TextureRect).modulate = color
(entry["label"] as Label).add_theme_color_override("font_color", color)
(entry["underline"] as ColorRect).color = Brand.ROYAL_BLUE if active else Color(0, 0, 0, 0)
func _build_footer() -> void:
var dark := UIThemeService.is_dark_mode()
footer_card = PanelContainer.new()
@@ -192,6 +275,8 @@ func _refresh_theme_controls() -> void:
balance_value_label.add_theme_color_override("font_color", Brand.text_color(dark))
coin_caption_label.add_theme_color_override("font_color", Brand.muted_color(dark))
inventory_label.add_theme_color_override("font_color", Brand.muted_color(dark))
_apply_tab_theme(dark)
section_subtitle.add_theme_color_override("font_color", Brand.muted_color(dark))
footer_card.add_theme_stylebox_override("panel", Brand.content_card_box(dark, false))
footer_icon_panel.add_theme_stylebox_override("panel", Brand.icon_circle_box(dark, Brand.ROYAL_BLUE, 18))
footer_heading.add_theme_color_override("font_color", Brand.text_color(dark))
@@ -209,28 +294,69 @@ func _refresh() -> void:
if typeof(product_variant) != TYPE_DICTIONARY:
continue
var product: Dictionary = product_variant
products_grid.add_child(_build_product_card(dark, product))
var sku := str(product.get("sku", ""))
var price := int(product.get("coinPrice", 0))
products_grid.add_child(_build_item_card(
dark,
ICON_BY_SKU.get(sku, "star"),
Brand.ROYAL_BLUE,
str(product.get("name", "Store item")),
str(product.get("description", "")),
FEATURES_BY_SKU.get(sku, []),
POPULAR_SKUS.has(sku),
price,
"Buy",
CreditsService.credits < price,
_ask_purchase.bind(sku, str(product.get("name", "item")), price)))
if CreditsService.qbank_reset_tokens > 0:
products_grid.add_child(_build_reset_card(dark))
products_grid.add_child(_build_item_card(
dark,
"rotate-ccw",
Brand.SUCCESS,
"Use QBank Reset Token",
"Question eligibility resets; ELO, history, bookmarks, notes, reviews, and earning limits are preserved.",
FEATURES_BY_SKU.get("qbank_reset_1", []),
false,
-1,
"Use token",
false,
_ask_reset))
func _build_product_card(dark: bool, product: Dictionary) -> PanelContainer:
var sku := str(product.get("sku", ""))
var price := int(product.get("coinPrice", 0))
func _build_item_card(
dark: bool,
icon_name: String,
accent: Color,
title: String,
description_text: String,
features: Array,
popular: bool,
price: int,
button_text: String,
button_disabled: bool,
on_press: Callable
) -> PanelContainer:
var card := _new_card(dark)
var content: VBoxContainer = card.get_child(0).get_child(0)
content.add_child(_card_icon(dark, ICON_BY_SKU.get(sku, "star"), Brand.ROYAL_BLUE))
var top_row := HBoxContainer.new()
content.add_child(top_row)
top_row.add_child(_card_icon(dark, icon_name, accent))
var badge_spacer := Control.new()
badge_spacer.size_flags_horizontal = Control.SIZE_EXPAND_FILL
top_row.add_child(badge_spacer)
if popular:
top_row.add_child(_popular_badge())
var name_label := Label.new()
name_label.text = str(product.get("name", "Store item"))
name_label.text = title
name_label.add_theme_font_override("font", Brand.font)
name_label.add_theme_font_size_override("font_size", 16)
name_label.add_theme_color_override("font_color", Brand.text_color(dark))
content.add_child(name_label)
var description := Label.new()
description.text = str(product.get("description", ""))
description.text = description_text
description.add_theme_font_override("font", Brand.font)
description.add_theme_font_size_override("font_size", 12)
description.add_theme_color_override("font_color", Brand.muted_color(dark))
@@ -238,59 +364,34 @@ func _build_product_card(dark: bool, product: Dictionary) -> PanelContainer:
description.custom_minimum_size = Vector2(190, 0)
content.add_child(description)
if not features.is_empty():
var bullets := VBoxContainer.new()
bullets.add_theme_constant_override("separation", 4)
content.add_child(bullets)
for feature_text in features:
bullets.add_child(_feature_row(dark, accent, str(feature_text)))
var divider := ColorRect.new()
divider.custom_minimum_size = Vector2(0, 1)
divider.color = Color("2a365a") if dark else Brand.LIGHT_BORDER
content.add_child(divider)
var footer_row := HBoxContainer.new()
footer_row.add_theme_constant_override("separation", 8)
content.add_child(footer_row)
footer_row.add_child(_price_tag(dark, price))
if price >= 0:
footer_row.add_child(_price_tag(dark, price))
var spacer := Control.new()
spacer.size_flags_horizontal = Control.SIZE_EXPAND_FILL
footer_row.add_child(spacer)
var buy := Button.new()
buy.text = "Buy"
buy.custom_minimum_size = Vector2(64, 34)
buy.add_theme_font_override("font", Brand.font)
_style_buy_button(buy)
buy.disabled = CreditsService.credits < price
buy.pressed.connect(_ask_purchase.bind(sku, str(product.get("name", "item")), price))
footer_row.add_child(buy)
return card
func _build_reset_card(dark: bool) -> PanelContainer:
var card := _new_card(dark)
var content: VBoxContainer = card.get_child(0).get_child(0)
content.add_child(_card_icon(dark, "rotate-ccw", Brand.SUCCESS))
var name_label := Label.new()
name_label.text = "Use QBank Reset Token"
name_label.add_theme_font_override("font", Brand.font)
name_label.add_theme_font_size_override("font_size", 16)
name_label.add_theme_color_override("font_color", Brand.text_color(dark))
content.add_child(name_label)
var description := Label.new()
description.text = "Question eligibility resets; ELO, history, bookmarks, notes, reviews, and earning limits are preserved."
description.add_theme_font_override("font", Brand.font)
description.add_theme_font_size_override("font_size", 12)
description.add_theme_color_override("font_color", Brand.muted_color(dark))
description.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
description.custom_minimum_size = Vector2(190, 0)
content.add_child(description)
var footer_row := HBoxContainer.new()
content.add_child(footer_row)
var spacer := Control.new()
spacer.size_flags_horizontal = Control.SIZE_EXPAND_FILL
footer_row.add_child(spacer)
var use_button := Button.new()
use_button.text = "Use token"
use_button.custom_minimum_size = Vector2(90, 34)
use_button.add_theme_font_override("font", Brand.font)
_style_buy_button(use_button)
use_button.pressed.connect(_ask_reset)
footer_row.add_child(use_button)
var button := Button.new()
button.text = button_text
button.custom_minimum_size = Vector2(64, 34)
button.add_theme_font_override("font", Brand.font)
_style_buy_button(button)
button.disabled = button_disabled
button.pressed.connect(on_press)
footer_row.add_child(button)
return card
@@ -326,6 +427,45 @@ func _card_icon(dark: bool, icon_name: String, accent: Color) -> PanelContainer:
return icon_panel
func _popular_badge() -> PanelContainer:
var badge := PanelContainer.new()
badge.size_flags_vertical = Control.SIZE_SHRINK_BEGIN
var box := StyleBoxFlat.new()
box.bg_color = Brand.ROYAL_BLUE
box.set_corner_radius_all(6)
box.content_margin_left = 8
box.content_margin_right = 8
box.content_margin_top = 3
box.content_margin_bottom = 3
badge.add_theme_stylebox_override("panel", box)
var label := Label.new()
label.text = "Popular"
label.add_theme_font_override("font", Brand.font)
label.add_theme_font_size_override("font_size", 11)
label.add_theme_color_override("font_color", Brand.DARK_TEXT)
badge.add_child(label)
return badge
func _feature_row(dark: bool, accent: Color, text: String) -> HBoxContainer:
var row := HBoxContainer.new()
row.add_theme_constant_override("separation", 6)
var icon := TextureRect.new()
icon.texture = load("res://assets/icons/lucide/circle-check.svg")
icon.custom_minimum_size = Vector2(14, 14)
icon.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
icon.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
icon.modulate = accent
row.add_child(icon)
var label := Label.new()
label.text = text
label.add_theme_font_override("font", Brand.font)
label.add_theme_font_size_override("font_size", 12)
label.add_theme_color_override("font_color", Brand.muted_color(dark))
row.add_child(label)
return row
func _price_tag(dark: bool, price: int) -> HBoxContainer:
var row := HBoxContainer.new()
row.add_theme_constant_override("separation", 4)