Refine creation tab UX, generation flow, and bindings
Large changes across frontend, backend and docs to align creation-tab and generation-page behavior with new product UI/UX and Spacetime bindings. Updated hermes decision-log and pitfalls with concrete rules (banner carousel, font sizing, unread-dot tokens, template-card layout, direct card->entry routing, separation of account balance vs prize pools, removal of global page card shell, generation progress milestones and unified circular progress, and background video handling). Added GenerationProgressHero component and media assets, plus generation-related UI/tests updates (CustomWorldGenerationView, BarkBattleGeneratingView, creation hub/cards, platform entry routing, index tests). Backend and contract updates include new category fields in admin API types and admin UI form/list, spacetime-client/module/migration changes and generated bindings script. Misc: many tests adjusted, new docs and plan files added, and several server-rs crate changes to support the updated creation/ generation workflows.
This commit is contained in:
@@ -1159,6 +1159,43 @@ where
|
||||
|
||||
fn normalize_migration_row(table_name: &str, value: &serde_json::Value) -> serde_json::Value {
|
||||
let mut next_value = value.clone();
|
||||
if table_name == "creation_entry_config" {
|
||||
if let Some(object) = next_value.as_object_mut() {
|
||||
// 中文注释:入口活动横幅字段晚于创作入口配置表加入,旧迁移包按运行态默认横幅兼容。
|
||||
object
|
||||
.entry("event_title".to_string())
|
||||
.or_insert(serde_json::Value::Null);
|
||||
object
|
||||
.entry("event_description".to_string())
|
||||
.or_insert(serde_json::Value::Null);
|
||||
object
|
||||
.entry("event_cover_image_src".to_string())
|
||||
.or_insert(serde_json::Value::Null);
|
||||
object
|
||||
.entry("event_prize_pool_mud_points".to_string())
|
||||
.or_insert_with(|| serde_json::Value::from(58_000));
|
||||
object
|
||||
.entry("event_starts_at_text".to_string())
|
||||
.or_insert(serde_json::Value::Null);
|
||||
object
|
||||
.entry("event_ends_at_text".to_string())
|
||||
.or_insert(serde_json::Value::Null);
|
||||
}
|
||||
}
|
||||
if table_name == "creation_entry_type_config" {
|
||||
if let Some(object) = next_value.as_object_mut() {
|
||||
// 中文注释:入口分类字段晚于入口类型配置表加入,旧迁移包按未分类兼容。
|
||||
object
|
||||
.entry("category_id".to_string())
|
||||
.or_insert(serde_json::Value::Null);
|
||||
object
|
||||
.entry("category_label".to_string())
|
||||
.or_insert(serde_json::Value::Null);
|
||||
object
|
||||
.entry("category_sort_order".to_string())
|
||||
.or_insert_with(|| serde_json::Value::from(0));
|
||||
}
|
||||
}
|
||||
if table_name == "user_account" {
|
||||
if let Some(object) = next_value.as_object_mut() {
|
||||
// 中文注释:头像字段晚于认证拆表加入,旧迁移包按未设置头像兼容。
|
||||
|
||||
@@ -11,6 +11,18 @@ pub struct CreationEntryConfig {
|
||||
pub(crate) modal_title: String,
|
||||
pub(crate) modal_description: String,
|
||||
pub(crate) updated_at: Timestamp,
|
||||
#[default(None::<String>)]
|
||||
pub(crate) event_title: Option<String>,
|
||||
#[default(None::<String>)]
|
||||
pub(crate) event_description: Option<String>,
|
||||
#[default(None::<String>)]
|
||||
pub(crate) event_cover_image_src: Option<String>,
|
||||
#[default(DEFAULT_CREATION_ENTRY_EVENT_PRIZE_POOL_MUD_POINTS)]
|
||||
pub(crate) event_prize_pool_mud_points: u64,
|
||||
#[default(None::<String>)]
|
||||
pub(crate) event_starts_at_text: Option<String>,
|
||||
#[default(None::<String>)]
|
||||
pub(crate) event_ends_at_text: Option<String>,
|
||||
}
|
||||
|
||||
#[spacetimedb::table(
|
||||
@@ -28,6 +40,12 @@ pub struct CreationEntryTypeConfig {
|
||||
pub(crate) open: bool,
|
||||
pub(crate) sort_order: i32,
|
||||
pub(crate) updated_at: Timestamp,
|
||||
#[default(None::<String>)]
|
||||
pub(crate) category_id: Option<String>,
|
||||
#[default(None::<String>)]
|
||||
pub(crate) category_label: Option<String>,
|
||||
#[default(0)]
|
||||
pub(crate) category_sort_order: i32,
|
||||
}
|
||||
|
||||
#[spacetimedb::procedure]
|
||||
@@ -88,6 +106,9 @@ fn upsert_creation_entry_type_config_in_tx(
|
||||
open: input.open,
|
||||
sort_order: input.sort_order,
|
||||
updated_at: now,
|
||||
category_id: Some(normalize_category_id(&input.category_id)),
|
||||
category_label: Some(normalize_category_label(&input.category_label)),
|
||||
category_sort_order: input.category_sort_order,
|
||||
};
|
||||
if ctx.db.creation_entry_type_config().id().find(&id).is_some() {
|
||||
ctx.db.creation_entry_type_config().id().update(row);
|
||||
@@ -120,6 +141,9 @@ fn get_or_seed_creation_entry_config_snapshot(
|
||||
visible: row.visible,
|
||||
open: row.open,
|
||||
sort_order: row.sort_order,
|
||||
category_id: normalize_optional_category_id(row.category_id.as_deref()),
|
||||
category_label: normalize_optional_category_label(row.category_label.as_deref()),
|
||||
category_sort_order: row.category_sort_order,
|
||||
updated_at_micros: row.updated_at.to_micros_since_unix_epoch(),
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
@@ -141,6 +165,29 @@ fn get_or_seed_creation_entry_config_snapshot(
|
||||
title: header.modal_title,
|
||||
description: header.modal_description,
|
||||
},
|
||||
event_banner: CreationEntryEventBannerSnapshot {
|
||||
title: normalize_optional_text(
|
||||
header.event_title.as_deref(),
|
||||
DEFAULT_CREATION_ENTRY_EVENT_TITLE,
|
||||
),
|
||||
description: normalize_optional_text(
|
||||
header.event_description.as_deref(),
|
||||
DEFAULT_CREATION_ENTRY_EVENT_DESCRIPTION,
|
||||
),
|
||||
cover_image_src: normalize_optional_text(
|
||||
header.event_cover_image_src.as_deref(),
|
||||
DEFAULT_CREATION_ENTRY_EVENT_COVER_IMAGE_SRC,
|
||||
),
|
||||
prize_pool_mud_points: header.event_prize_pool_mud_points,
|
||||
starts_at_text: normalize_optional_text(
|
||||
header.event_starts_at_text.as_deref(),
|
||||
DEFAULT_CREATION_ENTRY_EVENT_STARTS_AT_TEXT,
|
||||
),
|
||||
ends_at_text: normalize_optional_text(
|
||||
header.event_ends_at_text.as_deref(),
|
||||
DEFAULT_CREATION_ENTRY_EVENT_ENDS_AT_TEXT,
|
||||
),
|
||||
},
|
||||
creation_types,
|
||||
updated_at_micros: header.updated_at.to_micros_since_unix_epoch(),
|
||||
})
|
||||
@@ -164,6 +211,12 @@ fn seed_creation_entry_config_if_missing(ctx: &ReducerContext) {
|
||||
modal_title: DEFAULT_CREATION_ENTRY_MODAL_TITLE.to_string(),
|
||||
modal_description: DEFAULT_CREATION_ENTRY_MODAL_DESCRIPTION.to_string(),
|
||||
updated_at: now,
|
||||
event_title: Some(DEFAULT_CREATION_ENTRY_EVENT_TITLE.to_string()),
|
||||
event_description: Some(DEFAULT_CREATION_ENTRY_EVENT_DESCRIPTION.to_string()),
|
||||
event_cover_image_src: Some(DEFAULT_CREATION_ENTRY_EVENT_COVER_IMAGE_SRC.to_string()),
|
||||
event_prize_pool_mud_points: DEFAULT_CREATION_ENTRY_EVENT_PRIZE_POOL_MUD_POINTS,
|
||||
event_starts_at_text: Some(DEFAULT_CREATION_ENTRY_EVENT_STARTS_AT_TEXT.to_string()),
|
||||
event_ends_at_text: Some(DEFAULT_CREATION_ENTRY_EVENT_ENDS_AT_TEXT.to_string()),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -348,6 +401,43 @@ fn default_creation_entry_type_configs(now: Timestamp) -> Vec<CreationEntryTypeC
|
||||
open: snapshot.open,
|
||||
sort_order: snapshot.sort_order,
|
||||
updated_at: now,
|
||||
category_id: Some(snapshot.category_id),
|
||||
category_label: Some(snapshot.category_label),
|
||||
category_sort_order: snapshot.category_sort_order,
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn normalize_category_id(value: &str) -> String {
|
||||
let normalized = value.trim();
|
||||
if normalized.is_empty() {
|
||||
DEFAULT_CREATION_ENTRY_CATEGORY_ID.to_string()
|
||||
} else {
|
||||
normalized.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
fn normalize_category_label(value: &str) -> String {
|
||||
let normalized = value.trim();
|
||||
if normalized.is_empty() {
|
||||
DEFAULT_CREATION_ENTRY_CATEGORY_LABEL.to_string()
|
||||
} else {
|
||||
normalized.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
fn normalize_optional_category_id(value: Option<&str>) -> String {
|
||||
normalize_optional_text(value, DEFAULT_CREATION_ENTRY_CATEGORY_ID)
|
||||
}
|
||||
|
||||
fn normalize_optional_category_label(value: Option<&str>) -> String {
|
||||
normalize_optional_text(value, DEFAULT_CREATION_ENTRY_CATEGORY_LABEL)
|
||||
}
|
||||
|
||||
fn normalize_optional_text(value: Option<&str>, fallback: &str) -> String {
|
||||
value
|
||||
.map(str::trim)
|
||||
.filter(|normalized| !normalized.is_empty())
|
||||
.unwrap_or(fallback)
|
||||
.to_string()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user