commit 2f16e2ed5a9d04776e5c3aadaa82ab94238d9d39
parent 14211595b7980f5232c3184456ea82fd0c6f6a3e
Author: triesap <tyson@radroots.org>
Date: Mon, 20 Apr 2026 16:42:23 +0000
feat: add buyer shell routing and mode switching
Diffstat:
7 files changed, 664 insertions(+), 120 deletions(-)
diff --git a/crates/launchers/desktop/src/app.rs b/crates/launchers/desktop/src/app.rs
@@ -268,6 +268,7 @@ mod tests {
settings_account_projection: SettingsAccountProjection::default(),
startup_gate,
home_route,
+ personal_projection: Default::default(),
farm_setup_projection: Default::default(),
farm_readiness_projection: FarmWorkspaceReadinessProjection::default(),
today_projection: TodayAgendaProjection::default(),
@@ -363,7 +364,7 @@ mod tests {
);
assert_eq!(home_stage(&setup), HomeStage::Setup);
- assert_eq!(home_stage(&personal), HomeStage::PersonalHolding);
+ assert_eq!(home_stage(&personal), HomeStage::BuyerWorkspace);
assert_eq!(home_stage(&farmer), HomeStage::FarmerWorkspace);
assert_eq!(home_stage(&blocked), HomeStage::Setup);
}
diff --git a/crates/launchers/desktop/src/runtime.rs b/crates/launchers/desktop/src/runtime.rs
@@ -1,3 +1,4 @@
+use std::collections::BTreeSet;
use std::fmt;
use std::sync::{Arc, Mutex, MutexGuard, PoisonError};
@@ -7,9 +8,9 @@ use radroots_app_models::{
FarmId, FarmProfileRecord, FarmReadiness, FarmRulesProjection, FarmSetupDraft,
FarmSetupProjection, FarmSummary, FarmerSection, FulfillmentWindowId,
LoggedOutStartupProjection, OrderDetailProjection, OrderId, OrdersFilter, OrdersListProjection,
- OrdersScreenQueryState, PackDayProjection, PackDayScreenQueryState, PickupLocationRecord,
- ProductEditorDraft, ProductId, ProductsFilter, ProductsListProjection, ProductsSort,
- SettingsAccountProjection, SettingsPreference, SettingsSection, ShellSection,
+ OrdersScreenQueryState, PackDayProjection, PackDayScreenQueryState, PersonalSection,
+ PickupLocationRecord, ProductEditorDraft, ProductId, ProductsFilter, ProductsListProjection,
+ ProductsSort, SettingsAccountProjection, SettingsPreference, SettingsSection, ShellSection,
TodayAgendaProjection,
};
use radroots_app_remote_signer::{
@@ -20,10 +21,12 @@ use radroots_app_sqlite::{
derive_farm_rules_readiness,
};
use radroots_app_state::{
- AppShellProjection, AppStateCommand, AppStateStore, AppStateStoreError, FarmSetupFlowStage,
+ AppShellProjection, AppStateCommand, AppStateStore, AppStateStoreError,
+ BuyerBrowseScreenProjection, BuyerCartScreenProjection, BuyerOrdersScreenProjection,
+ BuyerSearchScreenProjection, BuyerSearchScreenQueryState, FarmSetupFlowStage,
FarmWorkspaceReadinessProjection, HomeRoute, InMemoryAppStateRepository,
- OrdersScreenProjection, PackDayScreenProjection, ProductsScreenProjection,
- ProductsScreenQueryState,
+ OrdersScreenProjection, PackDayScreenProjection, PersonalWorkspaceProjection,
+ ProductsScreenProjection, ProductsScreenQueryState,
};
use radroots_nostr_accounts::prelude::RadrootsNostrAccountsManager;
use thiserror::Error;
@@ -67,6 +70,7 @@ impl DesktopAppRuntime {
startup_gate: state.state_store.startup_gate(),
logged_out_startup: state.state_store.logged_out_startup_projection().clone(),
home_route: state.state_store.home_route(),
+ personal_projection: state.state_store.personal_projection().clone(),
farm_setup_projection: state.state_store.farm_setup_projection().clone(),
farm_readiness_projection: state.state_store.farm_readiness_projection().clone(),
today_projection: state.state_store.today_projection().clone(),
@@ -164,9 +168,8 @@ impl DesktopAppRuntime {
let mut state = self.lock_state_mut();
let selected_section = match state.state_store.startup_gate() {
AppStartupGate::Farmer => ShellSection::Farmer(FarmerSection::Today),
- AppStartupGate::Blocked | AppStartupGate::SetupRequired | AppStartupGate::Personal => {
- ShellSection::Home
- }
+ AppStartupGate::Blocked | AppStartupGate::SetupRequired => ShellSection::Home,
+ AppStartupGate::Personal => ShellSection::Personal(PersonalSection::Browse),
};
let section_changed = state
@@ -177,6 +180,10 @@ impl DesktopAppRuntime {
section_changed || editor_changed
}
+ pub fn select_personal_section(&self, section: PersonalSection) -> bool {
+ self.lock_state_mut().select_personal_section(section)
+ }
+
pub fn select_farmer_section(&self, section: FarmerSection) -> bool {
self.lock_state_mut().select_farmer_section(section)
}
@@ -419,6 +426,7 @@ pub struct DesktopAppRuntimeSummary {
pub startup_gate: AppStartupGate,
pub logged_out_startup: LoggedOutStartupProjection,
pub home_route: HomeRoute,
+ pub personal_projection: PersonalWorkspaceProjection,
pub farm_setup_projection: FarmSetupProjection,
pub farm_readiness_projection: FarmWorkspaceReadinessProjection,
pub today_projection: TodayAgendaProjection,
@@ -438,6 +446,7 @@ pub enum DesktopAppRuntimeActivityContextError {
#[derive(Clone, Debug, Default)]
struct DesktopSelectedAccountContext {
+ personal_projection: PersonalWorkspaceProjection,
farm_setup_projection: FarmSetupProjection,
farm_rules_projection: FarmRulesProjection,
today_projection: TodayAgendaProjection,
@@ -533,6 +542,9 @@ impl DesktopAppRuntimeState {
{
let _ = state_store.apply_in_memory(AppStateCommand::show_startup_signer_entry());
}
+ let _ = state_store.apply_in_memory(AppStateCommand::replace_personal_projection(
+ selected_account_context.personal_projection.clone(),
+ ));
let _ = state_store.apply_in_memory(AppStateCommand::replace_farm_rules_projection(
selected_account_context.farm_rules_projection,
));
@@ -709,6 +721,17 @@ impl DesktopAppRuntimeState {
}
}
+ fn select_personal_section(&mut self, section: PersonalSection) -> bool {
+ let section_changed = self
+ .state_store
+ .apply_in_memory(AppStateCommand::SelectSection(ShellSection::Personal(
+ section,
+ )));
+ let editor_changed = self.close_product_editor();
+
+ section_changed || editor_changed
+ }
+
fn set_products_search_query(&mut self, search_query: &str) -> Result<bool, AppSqliteError> {
let query = self.state_store.products_projection().query.clone();
if query.search_query == search_query {
@@ -1208,6 +1231,11 @@ impl DesktopAppRuntimeState {
}
fn apply_selected_account_context(&mut self, context: &DesktopSelectedAccountContext) -> bool {
+ let personal_changed =
+ self.state_store
+ .apply_in_memory(AppStateCommand::replace_personal_projection(
+ context.personal_projection.clone(),
+ ));
let farm_setup_changed =
self.state_store
.apply_in_memory(AppStateCommand::replace_farm_setup_projection(
@@ -1250,7 +1278,8 @@ impl DesktopAppRuntimeState {
};
let shell_changed = self.sync_truthful_farmer_section();
- farm_setup_changed
+ personal_changed
+ || farm_setup_changed
|| farm_rules_changed
|| today_changed
|| products_changed
@@ -1641,8 +1670,37 @@ fn load_selected_account_context(
selected_order_id: Option<OrderId>,
pack_day_query: PackDayScreenQueryState,
) -> Result<DesktopSelectedAccountContext, AppSqliteError> {
+ let buyer_context = identity_projection.buyer_context();
+ let buyer_fulfillment_methods = BTreeSet::new();
+ let buyer_listings = sqlite_store.load_buyer_listings("", &buyer_fulfillment_methods)?;
+ let buyer_cart = sqlite_store.load_buyer_cart(&buyer_context)?;
+ let buyer_checkout = sqlite_store.load_buyer_checkout(&buyer_context)?;
+ let buyer_orders = sqlite_store.load_buyer_orders(&buyer_context)?;
+ let personal_projection = PersonalWorkspaceProjection {
+ browse: BuyerBrowseScreenProjection {
+ listings: buyer_listings.clone(),
+ detail: None,
+ },
+ search: BuyerSearchScreenProjection {
+ query: BuyerSearchScreenQueryState::default(),
+ listings: buyer_listings,
+ detail: None,
+ },
+ cart: BuyerCartScreenProjection {
+ cart: buyer_cart,
+ checkout: buyer_checkout,
+ },
+ orders: BuyerOrdersScreenProjection {
+ list: buyer_orders,
+ detail: None,
+ },
+ ..PersonalWorkspaceProjection::default()
+ };
let Some(selected_account) = identity_projection.selected_account.as_ref() else {
- return Ok(DesktopSelectedAccountContext::default());
+ return Ok(DesktopSelectedAccountContext {
+ personal_projection,
+ ..DesktopSelectedAccountContext::default()
+ });
};
let farm_setup_projection =
sqlite_store.load_farm_setup(&selected_account.account.account_id)?;
@@ -1690,6 +1748,7 @@ fn load_selected_account_context(
};
Ok(DesktopSelectedAccountContext {
+ personal_projection,
farm_setup_projection,
farm_rules_projection,
today_projection,
@@ -1817,10 +1876,10 @@ mod tests {
FarmProfileRecord, FarmReadiness, FarmReadinessBlocker, FarmSetupDraft,
FarmSetupProjection, FarmSummary, FarmerActivationProjection, FarmerSection,
FulfillmentWindowId, FulfillmentWindowRecord, LoggedOutStartupProjection, OrderId,
- OrderStatus, OrdersFilter, PickupLocationId, PickupLocationRecord, ProductEditorDraft,
- ProductStatus, ProductsFilter, ProductsSort, SelectedSurfaceProjection, SettingsPreference,
- SettingsSection, ShellSection, TodayAgendaProjection, TodaySetupTask, TodaySetupTaskKind,
- TodaySummary,
+ OrderStatus, OrdersFilter, PersonalSection, PickupLocationId, PickupLocationRecord,
+ ProductEditorDraft, ProductStatus, ProductsFilter, ProductsSort, SelectedSurfaceProjection,
+ SettingsPreference, SettingsSection, ShellSection, TodayAgendaProjection, TodaySetupTask,
+ TodaySetupTaskKind, TodaySummary,
};
use radroots_app_remote_signer::{
RadrootsAppRemoteSignerPendingSession, RadrootsAppRemoteSignerSessionRecord,
@@ -2419,6 +2478,24 @@ mod tests {
}
#[test]
+ fn guest_marketplace_entry_selects_personal_browse_without_an_account() {
+ let runtime = memory_runtime();
+
+ assert!(runtime.select_personal_section(PersonalSection::Browse));
+
+ let summary = runtime.summary();
+ assert_eq!(summary.startup_gate, AppStartupGate::SetupRequired);
+ assert_eq!(
+ summary.shell_projection.selected_section,
+ ShellSection::Personal(PersonalSection::Browse)
+ );
+ assert_eq!(
+ summary.personal_projection.entry.state,
+ radroots_app_models::PersonalEntryState::Guest
+ );
+ }
+
+ #[test]
fn runtime_products_queries_refresh_the_repository_backed_projection() {
let runtime = memory_runtime();
diff --git a/crates/launchers/desktop/src/source_guards.rs b/crates/launchers/desktop/src/source_guards.rs
@@ -9,6 +9,7 @@ const ALLOWED_MENU_LITERALS: &[&str] = &["cmd-q", "settings window should open"]
const ALLOWED_WINDOW_LITERALS: &[&str] = &[
"",
" ",
+ " from {farm}",
"${dollars}.{cents:02} / {}",
", ",
"0",
@@ -44,12 +45,15 @@ const ALLOWED_WINDOW_LITERALS: &[&str] = &[
"failed to route into orders view",
"failed to save farm settings projection",
"failed to save product editor draft",
+ "failed to switch into farm mode",
+ "failed to switch into marketplace mode",
"failed to update orders filter",
"failed to route into products view",
"failed to update product stock",
"failed to update products filter",
"failed to update products search query",
"failed to update products sort",
+ "home-browse-marketplace",
"home-connect-signer",
"home-connect-signer-submit",
"home-continue",
@@ -66,6 +70,14 @@ const ALLOWED_WINDOW_LITERALS: &[&str] = &[
"home-nav-today",
"home-orders-scroll",
"home-pack-day-scroll",
+ "buyer-browse-scroll",
+ "buyer-cart-scroll",
+ "buyer-nav-browse",
+ "buyer-nav-cart",
+ "buyer-nav-orders",
+ "buyer-nav-search",
+ "buyer-orders-scroll",
+ "buyer-search-scroll",
"home-today-open-pack-day",
"home-today-order-open",
"home-signer-back",
@@ -135,6 +147,13 @@ const ALLOWED_WINDOW_LITERALS: &[&str] = &[
"remote signer connection failed: relay refused the request",
"remote signer did not respond yet",
"runtime unavailable",
+ "shell",
+ "shell-account-entry",
+ "shell-account-label",
+ "shell-mode-farm",
+ "shell-mode-marketplace",
+ "shell.switch_farm_failed",
+ "shell.switch_marketplace_failed",
"settings",
"settings-add-blackout-period",
"settings-add-fulfillment-window",
@@ -161,12 +180,22 @@ const ALLOWED_WINDOW_LITERALS: &[&str] = &[
"startup-title-radroots",
"startup-title-starting",
"wss://relay.radroots.example",
+ "{} items are ready in your cart{}.",
+ "{} local listings are already loaded on this device.",
+ "{} local listings are available to search from the shared marketplace source.",
+ "{} local orders are already available on this device.",
"{quantity} {unit_label}",
"{} {}",
];
const REQUIRED_WINDOW_COPY_KEYS: &[&str] = &[
+ "AppTextKey::AppName",
+ "AppTextKey::HomeHeaderMarketplaceMode",
+ "AppTextKey::HomeHeaderFarmMode",
+ "AppTextKey::HomeHeaderAccountSetupAction",
+ "AppTextKey::HomeHeaderGuestLabel",
"AppTextKey::HomeSetupBackAction",
+ "AppTextKey::HomeSetupBrowseMarketplaceAction",
"AppTextKey::HomeSetupConnectSignerAction",
"AppTextKey::HomeSetupContinueAction",
"AppTextKey::HomeSetupGenerateKeyAction",
@@ -201,9 +230,16 @@ const REQUIRED_WINDOW_COPY_KEYS: &[&str] = &[
"AppTextKey::HomeFarmSetupFinishAction",
"AppTextKey::HomeFarmSetupContinueAction",
"AppTextKey::HomeTodayOpenInProductsAction",
+ "AppTextKey::HomeNavBrowse",
+ "AppTextKey::HomeNavSearch",
+ "AppTextKey::HomeNavCart",
"AppTextKey::HomeNavToday",
"AppTextKey::HomeNavProducts",
"AppTextKey::HomeNavOrders",
+ "AppTextKey::PersonalBrowsePlaceholderBody",
+ "AppTextKey::PersonalSearchPlaceholderBody",
+ "AppTextKey::PersonalCartPlaceholderBody",
+ "AppTextKey::PersonalOrdersPlaceholderBody",
"AppTextKey::HomeTodayOpenInOrdersAction",
"AppTextKey::HomeTodayOpenInPackDayAction",
"AppTextKey::OrdersTitle",
diff --git a/crates/launchers/desktop/src/window.rs b/crates/launchers/desktop/src/window.rs
@@ -17,10 +17,10 @@ use radroots_app_models::{
FarmerSection, FulfillmentWindowId, FulfillmentWindowRecord, FulfillmentWindowSummary,
LoggedOutStartupPhase, OrderDetailItemRow, OrderDetailProjection, OrderId, OrderListRow,
OrderPrimaryAction, OrderStatus, OrdersFilter, OrdersListRow, PackDayPackListRow,
- PackDayProductTotalRow, PackDayRosterRow, PickupLocationId, PickupLocationRecord,
- ProductAttentionState, ProductEditorDraft, ProductId, ProductListRow, ProductPublishBlocker,
- ProductStatus, ProductsFilter, ProductsListRow, ProductsSort, ShellSection,
- TodayAgendaProjection, TodaySetupTaskKind,
+ PackDayProductTotalRow, PackDayRosterRow, PersonalEntryState, PersonalSection,
+ PickupLocationId, PickupLocationRecord, ProductAttentionState, ProductEditorDraft, ProductId,
+ ProductListRow, ProductPublishBlocker, ProductStatus, ProductsFilter, ProductsListRow,
+ ProductsSort, ShellSection, TodayAgendaProjection, TodaySetupTaskKind,
};
use radroots_app_remote_signer::{
RadrootsAppRemoteSignerApprovedSession, RadrootsAppRemoteSignerPendingPollOutcome,
@@ -47,7 +47,7 @@ use radroots_app_ui::{
app_split_shell, app_stack_h, app_stack_v, app_status_indicator as status_indicator,
app_surface_card, app_surface_card_section as home_card, app_surface_panel,
app_surface_sidebar, app_surface_window as app_window_shell,
- app_text_badge as settings_badge_text, app_text_body_subtle as home_body_text,
+ app_text_badge as settings_badge_text, app_text_body_subtle as home_body_text, app_text_label,
app_text_label as home_farm_setup_field_label, app_text_value, label_value_list,
utility_title_row,
};
@@ -85,7 +85,7 @@ pub enum PrimaryWindowTarget {
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum HomeStage {
Setup,
- PersonalHolding,
+ BuyerWorkspace,
FarmerWorkspace,
}
@@ -94,17 +94,18 @@ pub fn primary_window_target(_: &DesktopAppRuntimeSummary) -> PrimaryWindowTarge
}
pub fn home_stage(summary: &DesktopAppRuntimeSummary) -> HomeStage {
- if summary.startup_issue.is_some()
- || matches!(
- summary.startup_gate,
- AppStartupGate::Blocked | AppStartupGate::SetupRequired
- )
- {
+ if summary.startup_issue.is_some() || summary.startup_gate == AppStartupGate::Blocked {
HomeStage::Setup
} else if summary.startup_gate == AppStartupGate::Farmer {
HomeStage::FarmerWorkspace
+ } else if matches!(
+ summary.shell_projection.selected_section,
+ ShellSection::Personal(_)
+ ) || summary.startup_gate == AppStartupGate::Personal
+ {
+ HomeStage::BuyerWorkspace
} else {
- HomeStage::PersonalHolding
+ HomeStage::Setup
}
}
@@ -183,7 +184,6 @@ pub struct HomeView {
startup_signer_connect_state: StartupSignerConnectState,
startup_signer_task_token: u64,
startup_signer_recovery_attempted: bool,
- logged_in_view: LoggedInHomeView,
farm_setup_form: Option<FarmSetupFormState>,
products_search: Option<ProductsSearchState>,
products_stock_editor: Option<ProductsStockEditorState>,
@@ -229,7 +229,6 @@ impl HomeView {
startup_signer_connect_state: StartupSignerConnectState::Idle,
startup_signer_task_token: 0,
startup_signer_recovery_attempted: false,
- logged_in_view: LoggedInHomeView::new(),
farm_setup_form: None,
products_search: None,
products_stock_editor: None,
@@ -821,6 +820,66 @@ impl HomeView {
}
}
+ fn select_personal_section(&mut self, section: PersonalSection, cx: &mut Context<Self>) {
+ if self.runtime.select_personal_section(section) {
+ self.products_stock_editor = None;
+ self.product_editor_form = None;
+ cx.notify();
+ }
+ }
+
+ fn switch_to_marketplace(&mut self, cx: &mut Context<Self>) {
+ match self
+ .runtime
+ .select_active_surface(radroots_app_models::ActiveSurface::Personal)
+ {
+ Ok(true) => {
+ self.products_stock_editor = None;
+ self.product_editor_form = None;
+ cx.notify();
+ }
+ Ok(false) => {}
+ Err(runtime_error) => {
+ error!(
+ target: "shell",
+ event = "shell.switch_marketplace_failed",
+ error = %runtime_error,
+ "failed to switch into marketplace mode"
+ );
+ }
+ }
+ }
+
+ fn switch_to_farmer_workspace(&mut self, cx: &mut Context<Self>) {
+ match self
+ .runtime
+ .select_active_surface(radroots_app_models::ActiveSurface::Farmer)
+ {
+ Ok(true) => {
+ self.products_stock_editor = None;
+ self.product_editor_form = None;
+ cx.notify();
+ }
+ Ok(false) => {}
+ Err(runtime_error) => {
+ error!(
+ target: "shell",
+ event = "shell.switch_farm_failed",
+ error = %runtime_error,
+ "failed to switch into farm mode"
+ );
+ }
+ }
+ }
+
+ fn open_account_entry(&mut self, cx: &mut Context<Self>) {
+ if self.runtime.select_home() {
+ self.products_stock_editor = None;
+ self.product_editor_form = None;
+ cx.notify();
+ }
+ }
+
fn handle_startup_signer_input_event(
&mut self,
state: &Entity<InputState>,
@@ -1668,6 +1727,60 @@ impl HomeView {
.into_any_element()
}
+ fn render_buyer_workspace(
+ &mut self,
+ runtime: &DesktopAppRuntimeSummary,
+ cx: &mut Context<Self>,
+ ) -> AnyElement {
+ let selected_personal_section = selected_personal_section(runtime);
+ let main_content = match selected_personal_section {
+ PersonalSection::Browse => buyer_browse_placeholder(runtime).into_any_element(),
+ PersonalSection::Search => buyer_search_placeholder(runtime).into_any_element(),
+ PersonalSection::Cart => buyer_cart_placeholder(runtime).into_any_element(),
+ PersonalSection::Orders => buyer_orders_placeholder(runtime).into_any_element(),
+ };
+
+ app_split_shell(
+ buyer_sidebar(
+ runtime,
+ cx.listener(|this, _, _, cx| {
+ this.select_personal_section(PersonalSection::Browse, cx)
+ }),
+ cx.listener(|this, _, _, cx| {
+ this.select_personal_section(PersonalSection::Search, cx)
+ }),
+ cx.listener(|this, _, _, cx| {
+ this.select_personal_section(PersonalSection::Cart, cx)
+ }),
+ cx.listener(|this, _, _, cx| {
+ this.select_personal_section(PersonalSection::Orders, cx)
+ }),
+ cx,
+ )
+ .into_any_element(),
+ app_stack_v(APP_UI_THEME.shells.home_stack_gap_px)
+ .size_full()
+ .child(shared_shell_header(
+ runtime,
+ cx.listener(|this, _, _, cx| this.switch_to_marketplace(cx)),
+ cx.listener(|this, _, _, cx| this.switch_to_farmer_workspace(cx)),
+ cx.listener(|this, _, _, cx| this.open_account_entry(cx)),
+ cx,
+ ))
+ .child(
+ app_scroll_panel(
+ buyer_content_scroll_id(selected_personal_section),
+ 0.0,
+ None,
+ main_content,
+ )
+ .into_any_element(),
+ )
+ .into_any_element(),
+ )
+ .into_any_element()
+ }
+
fn render_farmer_workspace(
&mut self,
runtime: &DesktopAppRuntimeSummary,
@@ -1703,13 +1816,25 @@ impl HomeView {
cx,
)
.into_any_element(),
- app_scroll_panel(
- home_content_scroll_id(selected_farmer_section),
- 0.0,
- None,
- main_content,
- )
- .into_any_element(),
+ app_stack_v(APP_UI_THEME.shells.home_stack_gap_px)
+ .size_full()
+ .child(shared_shell_header(
+ runtime,
+ cx.listener(|this, _, _, cx| this.switch_to_marketplace(cx)),
+ cx.listener(|this, _, _, cx| this.switch_to_farmer_workspace(cx)),
+ cx.listener(|this, _, _, cx| this.open_account_entry(cx)),
+ cx,
+ ))
+ .child(
+ app_scroll_panel(
+ home_content_scroll_id(selected_farmer_section),
+ 0.0,
+ None,
+ main_content,
+ )
+ .into_any_element(),
+ )
+ .into_any_element(),
)
.into_any_element()
}
@@ -2240,6 +2365,9 @@ impl Render for HomeView {
self.startup_signer_entry.as_ref(),
&self.startup_signer_connect_state,
cx.listener(|this, _, _, cx| this.show_startup_identity_choice(cx)),
+ cx.listener(|this, _, _, cx| {
+ this.select_personal_section(PersonalSection::Browse, cx)
+ }),
cx.listener(|this, _, window, cx| this.start_generate_key(window, cx)),
cx.listener(|this, _, _, cx| this.show_startup_signer_entry(cx)),
cx.listener(|this, _, window, cx| this.submit_startup_signer(window, cx)),
@@ -2247,10 +2375,7 @@ impl Render for HomeView {
cx,
)
.into_any_element(),
- HomeStage::PersonalHolding => self
- .logged_in_view
- .render_holding(&runtime_summary)
- .into_any_element(),
+ HomeStage::BuyerWorkspace => self.render_buyer_workspace(&runtime_summary, cx),
HomeStage::FarmerWorkspace => self.render_farmer_workspace(&runtime_summary, cx),
}
}
@@ -2580,6 +2705,7 @@ impl StartupHomeView {
signer_entry: Option<&StartupSignerEntryState>,
connect_state: &StartupSignerConnectState,
on_continue: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
+ on_browse_marketplace: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
on_generate_key: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
on_connect_signer: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
on_submit_signer: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
@@ -2592,6 +2718,7 @@ impl StartupHomeView {
signer_entry,
connect_state,
on_continue,
+ on_browse_marketplace,
on_generate_key,
on_connect_signer,
on_submit_signer,
@@ -2601,18 +2728,6 @@ impl StartupHomeView {
}
}
-struct LoggedInHomeView;
-
-impl LoggedInHomeView {
- fn new() -> Self {
- Self
- }
-
- fn render_holding(&self, runtime: &DesktopAppRuntimeSummary) -> AnyElement {
- holding_home_shell(runtime).into_any_element()
- }
-}
-
struct SettingsPickupLocationFormState {
pickup_location_id: PickupLocationId,
label_input: Entity<InputState>,
@@ -4745,43 +4860,212 @@ const SETTINGS_OPERATIONS_PANEL_SECTIONS: &[SettingsInventorySectionSpec] = &[
},
];
-fn holding_home_shell(runtime: &DesktopAppRuntimeSummary) -> impl IntoElement {
- let home_status = home_status_presentation(runtime);
- let (title_key, body_key) = match home_stage(runtime) {
- HomeStage::Setup => (
- AppTextKey::HomeTodayEmptySetupTitle,
- AppTextKey::HomeTodayEmptySetupBody,
- ),
- HomeStage::PersonalHolding => (
- AppTextKey::HomeTodayEmptyNoFarmTitle,
- AppTextKey::HomeTodayEmptyNoFarmBody,
- ),
- HomeStage::FarmerWorkspace => (
- AppTextKey::HomeTodayEmptyQuietTitle,
- AppTextKey::HomeTodayEmptyQuietBody,
- ),
- };
- let mut sections = vec![home_empty_state_card(title_key, body_key).into_any_element()];
+fn shared_shell_header(
+ runtime: &DesktopAppRuntimeSummary,
+ on_select_marketplace: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
+ on_select_farm: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
+ on_open_account: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
+ cx: &App,
+) -> impl IntoElement {
+ let can_enter_farmer_workspace = runtime.personal_projection.entry.can_enter_farmer_workspace;
+ let is_marketplace_active =
+ runtime.shell_projection.active_surface != radroots_app_models::ActiveSurface::Farmer;
+ let farm_name = home_saved_farm(runtime).map(|farm| farm.display_name.clone());
+ let account_label = shell_account_label(runtime);
- if let Some(issue) = runtime.startup_issue.as_ref() {
- sections.push(
- home_card(
- app_shared_text(AppTextKey::MetadataStartupIssue),
- home_body_text(issue.clone()),
+ app_surface_panel(
+ div()
+ .w_full()
+ .px(px(APP_UI_THEME.shells.home_card_padding_px))
+ .py(px(APP_UI_THEME.foundation.spacing.small_px))
+ .flex()
+ .justify_between()
+ .items_center()
+ .gap(px(APP_UI_THEME.shells.home_stack_gap_px))
+ .child(
+ div()
+ .flex()
+ .flex_col()
+ .gap(px(2.0))
+ .child(app_text_label(app_shared_text(AppTextKey::AppName)))
+ .when_some(farm_name, |this, farm_name| {
+ this.child(home_body_text(farm_name))
+ }),
)
- .into_any_element(),
- );
+ .child(
+ app_cluster(APP_UI_THEME.foundation.spacing.small_px)
+ .items_center()
+ .when(can_enter_farmer_workspace, |this| {
+ this.child(
+ shared_shell_mode_button(
+ "shell-mode-marketplace",
+ AppTextKey::HomeHeaderMarketplaceMode,
+ is_marketplace_active,
+ on_select_marketplace,
+ cx,
+ )
+ .into_any_element(),
+ )
+ .child(
+ shared_shell_mode_button(
+ "shell-mode-farm",
+ AppTextKey::HomeHeaderFarmMode,
+ !is_marketplace_active,
+ on_select_farm,
+ cx,
+ )
+ .into_any_element(),
+ )
+ })
+ .child(shell_account_entry(
+ runtime,
+ account_label,
+ on_open_account,
+ cx,
+ )),
+ ),
+ )
+}
+
+fn shared_shell_mode_button(
+ id: &'static str,
+ key: AppTextKey,
+ is_active: bool,
+ on_click: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
+ cx: &App,
+) -> AnyElement {
+ if is_active {
+ div()
+ .id(id)
+ .text_size(px(APP_UI_THEME.foundation.typography.body_text_px))
+ .font_weight(gpui::FontWeight::SEMIBOLD)
+ .text_color(rgb(APP_UI_THEME.foundation.text.primary))
+ .child(app_shared_text(key))
+ .into_any_element()
+ } else {
+ action_button_compact(id, app_shared_text(key), on_click, cx).into_any_element()
}
+}
- app_split_shell(
- holding_home_sidebar(runtime).into_any_element(),
- app_stack_v(APP_UI_THEME.shells.home_stack_gap_px)
- .w_full()
- .max_w(px(APP_UI_THEME.shells.home_card_max_width_px))
- .mx_auto()
- .child(home_status_row(&home_status))
- .children(sections)
- .into_any_element(),
+fn shell_account_entry(
+ runtime: &DesktopAppRuntimeSummary,
+ account_label: String,
+ on_open_account: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
+ cx: &App,
+) -> AnyElement {
+ if runtime.personal_projection.entry.state == PersonalEntryState::Guest {
+ action_button_compact(
+ "shell-account-entry",
+ app_shared_text(AppTextKey::HomeHeaderAccountSetupAction),
+ on_open_account,
+ cx,
+ )
+ .into_any_element()
+ } else {
+ div()
+ .id("shell-account-label")
+ .text_size(px(APP_UI_THEME.foundation.typography.body_text_px))
+ .font_weight(gpui::FontWeight::MEDIUM)
+ .text_color(rgb(APP_UI_THEME.foundation.text.secondary))
+ .child(account_label)
+ .into_any_element()
+ }
+}
+
+fn shell_account_label(runtime: &DesktopAppRuntimeSummary) -> String {
+ runtime
+ .settings_account_projection
+ .selected_account
+ .as_ref()
+ .and_then(|account| {
+ account
+ .account
+ .label
+ .as_ref()
+ .map(|label| label.trim().to_owned())
+ .filter(|label| !label.is_empty())
+ .or_else(|| Some(account.account.npub.clone()))
+ })
+ .unwrap_or_else(|| app_shared_text(AppTextKey::HomeHeaderGuestLabel).to_string())
+}
+
+fn buyer_surface_placeholder(
+ title_key: AppTextKey,
+ body_key: AppTextKey,
+ detail: Option<String>,
+) -> AnyElement {
+ app_stack_v(APP_UI_THEME.shells.home_stack_gap_px)
+ .w_full()
+ .max_w(px(APP_UI_THEME.shells.home_card_max_width_px))
+ .mx_auto()
+ .child(app_text_value(app_shared_text(title_key)))
+ .child(app_surface_card(
+ app_stack_v(APP_UI_THEME.shells.home_stack_gap_px)
+ .w_full()
+ .child(home_body_text(app_shared_text(body_key)))
+ .when_some(detail, |this, detail| this.child(home_body_text(detail))),
+ ))
+ .into_any_element()
+}
+
+fn buyer_browse_placeholder(runtime: &DesktopAppRuntimeSummary) -> AnyElement {
+ let detail = (!runtime.personal_projection.browse.listings.rows.is_empty()).then_some(format!(
+ "{} local listings are already loaded on this device.",
+ runtime.personal_projection.browse.listings.rows.len()
+ ));
+
+ buyer_surface_placeholder(
+ AppTextKey::HomeNavBrowse,
+ AppTextKey::PersonalBrowsePlaceholderBody,
+ detail,
+ )
+}
+
+fn buyer_search_placeholder(runtime: &DesktopAppRuntimeSummary) -> AnyElement {
+ let detail = (!runtime.personal_projection.search.listings.rows.is_empty()).then_some(format!(
+ "{} local listings are available to search from the shared marketplace source.",
+ runtime.personal_projection.search.listings.rows.len()
+ ));
+
+ buyer_surface_placeholder(
+ AppTextKey::HomeNavSearch,
+ AppTextKey::PersonalSearchPlaceholderBody,
+ detail,
+ )
+}
+
+fn buyer_cart_placeholder(runtime: &DesktopAppRuntimeSummary) -> AnyElement {
+ let cart = &runtime.personal_projection.cart.cart;
+ let detail = if cart.lines.is_empty() {
+ None
+ } else {
+ Some(format!(
+ "{} items are ready in your cart{}.",
+ cart.lines.len(),
+ cart.farm_display_name
+ .as_ref()
+ .map(|farm| format!(" from {farm}"))
+ .unwrap_or_default()
+ ))
+ };
+
+ buyer_surface_placeholder(
+ AppTextKey::HomeNavCart,
+ AppTextKey::PersonalCartPlaceholderBody,
+ detail,
+ )
+}
+
+fn buyer_orders_placeholder(runtime: &DesktopAppRuntimeSummary) -> AnyElement {
+ let detail = (!runtime.personal_projection.orders.list.rows.is_empty()).then_some(format!(
+ "{} local orders are already available on this device.",
+ runtime.personal_projection.orders.list.rows.len()
+ ));
+
+ buyer_surface_placeholder(
+ AppTextKey::HomeNavOrders,
+ AppTextKey::PersonalOrdersPlaceholderBody,
+ detail,
)
}
@@ -4813,6 +5097,7 @@ fn startup_home_shell(
signer_entry: Option<&StartupSignerEntryState>,
connect_state: &StartupSignerConnectState,
on_continue: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
+ on_browse_marketplace: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
on_generate_key: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
on_connect_signer: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
on_submit_signer: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
@@ -4845,26 +5130,33 @@ fn startup_home_shell(
.child(startup_home_title(surface))
.child(startup_home_tagline())
.child(match surface {
- StartupHomeSurface::ContinuePrompt => {
- app_stack_v(APP_UI_THEME.shells.startup_stack_gap_px)
- .items_center()
- .child(action_button_primary(
- "home-continue",
- app_shared_text(
- AppTextKey::HomeSetupContinueAction,
- ),
- on_continue,
- cx,
- ))
- .when_some(startup_notice, |this, error| {
- this.child(
- div().w_full().text_center().child(
- home_body_text(error.to_owned()),
- ),
- )
- })
- .into_any_element()
- }
+ StartupHomeSurface::ContinuePrompt => app_stack_v(
+ APP_UI_THEME.shells.startup_stack_gap_px,
+ )
+ .items_center()
+ .child(action_button_primary(
+ "home-continue",
+ app_shared_text(AppTextKey::HomeSetupContinueAction),
+ on_continue,
+ cx,
+ ))
+ .child(action_button(
+ "home-browse-marketplace",
+ app_shared_text(
+ AppTextKey::HomeSetupBrowseMarketplaceAction,
+ ),
+ on_browse_marketplace,
+ cx,
+ ))
+ .when_some(startup_notice, |this, error| {
+ this.child(
+ div()
+ .w_full()
+ .text_center()
+ .child(home_body_text(error.to_owned())),
+ )
+ })
+ .into_any_element(),
StartupHomeSurface::IdentityChoice => {
app_stack_v(APP_UI_THEME.shells.startup_stack_gap_px)
.items_center()
@@ -5353,7 +5645,16 @@ fn home_sidebar(
)
}
-fn holding_home_sidebar(runtime: &DesktopAppRuntimeSummary) -> impl IntoElement {
+fn buyer_sidebar(
+ runtime: &DesktopAppRuntimeSummary,
+ on_select_browse: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
+ on_select_search: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
+ on_select_cart: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
+ on_select_orders: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
+ cx: &App,
+) -> impl IntoElement {
+ let selected_section = selected_personal_section(runtime);
+
app_surface_sidebar(
div()
.h_full()
@@ -5367,7 +5668,46 @@ fn holding_home_sidebar(runtime: &DesktopAppRuntimeSummary) -> impl IntoElement
.flex()
.flex_col()
.gap(px(APP_UI_THEME.shells.home_stack_gap_px))
- .child(app_text_value(app_shared_text(AppTextKey::HomeTodayTitle))),
+ .child(
+ buyer_sidebar_nav_button(
+ "buyer-nav-browse",
+ AppTextKey::HomeNavBrowse,
+ selected_section == PersonalSection::Browse,
+ on_select_browse,
+ cx,
+ )
+ .into_any_element(),
+ )
+ .child(
+ buyer_sidebar_nav_button(
+ "buyer-nav-search",
+ AppTextKey::HomeNavSearch,
+ selected_section == PersonalSection::Search,
+ on_select_search,
+ cx,
+ )
+ .into_any_element(),
+ )
+ .child(
+ buyer_sidebar_nav_button(
+ "buyer-nav-cart",
+ AppTextKey::HomeNavCart,
+ selected_section == PersonalSection::Cart,
+ on_select_cart,
+ cx,
+ )
+ .into_any_element(),
+ )
+ .child(
+ buyer_sidebar_nav_button(
+ "buyer-nav-orders",
+ AppTextKey::HomeNavOrders,
+ selected_section == PersonalSection::Orders,
+ on_select_orders,
+ cx,
+ )
+ .into_any_element(),
+ ),
)
.child(
div().child(div().when_some(home_saved_farm(runtime), |this, farm| {
@@ -5377,6 +5717,26 @@ fn holding_home_sidebar(runtime: &DesktopAppRuntimeSummary) -> impl IntoElement
)
}
+fn buyer_sidebar_nav_button(
+ id: &'static str,
+ key: AppTextKey,
+ is_active: bool,
+ on_click: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
+ cx: &App,
+) -> AnyElement {
+ if is_active {
+ div()
+ .id(id)
+ .text_size(px(APP_UI_THEME.foundation.typography.body_text_px * 2.0))
+ .font_weight(gpui::FontWeight::BOLD)
+ .text_color(rgb(APP_UI_THEME.foundation.text.primary))
+ .child(app_shared_text(key))
+ .into_any_element()
+ } else {
+ action_button(id, app_shared_text(key), on_click, cx).into_any_element()
+ }
+}
+
fn home_sidebar_navigation_sections(
selected_section: FarmerSection,
workspace_available: bool,
@@ -5411,6 +5771,15 @@ fn selected_farmer_section(runtime: &DesktopAppRuntimeSummary) -> FarmerSection
}
}
+fn selected_personal_section(runtime: &DesktopAppRuntimeSummary) -> PersonalSection {
+ match runtime.shell_projection.selected_section {
+ ShellSection::Personal(section) => section,
+ ShellSection::Home | ShellSection::Farmer(_) | ShellSection::Settings(_) => {
+ PersonalSection::Browse
+ }
+ }
+}
+
fn farmer_products_available(runtime: &DesktopAppRuntimeSummary) -> bool {
runtime.farm_setup_projection.has_saved_farm()
}
@@ -5432,6 +5801,15 @@ fn home_content_scroll_id(section: FarmerSection) -> &'static str {
}
}
+fn buyer_content_scroll_id(section: PersonalSection) -> &'static str {
+ match section {
+ PersonalSection::Browse => "buyer-browse-scroll",
+ PersonalSection::Search => "buyer-search-scroll",
+ PersonalSection::Cart => "buyer-cart-scroll",
+ PersonalSection::Orders => "buyer-orders-scroll",
+ }
+}
+
fn home_sidebar_nav_button(
id: &'static str,
key: AppTextKey,
@@ -7639,23 +8017,26 @@ fn home_farm_order_method_label_key(method: FarmOrderMethod) -> AppTextKey {
#[cfg(test)]
mod tests {
use super::{
- AppTextKey, FarmerHomeFarmState, SETTINGS_FARM_PANEL_SECTIONS, SETTINGS_NAVIGATION_ORDER,
- SETTINGS_OPERATIONS_PANEL_SECTIONS, SettingsInventorySectionSpec, SettingsPanelViewKey,
- StartupHomeSurface, StartupSignerConnectState, farm_setup_onboarding_card_spec,
- farmer_home_farm_state, farmer_pack_day_available, home_content_scroll_id, home_saved_farm,
- home_sidebar_navigation_sections, home_window_launch_size_px, home_window_minimum_size_px,
- parse_optional_product_editor_stock_input, parse_product_editor_price_input,
- product_display_title, startup_home_surface, startup_signer_preview_summary,
- startup_signer_preview_summary_for_connect_state, startup_signer_source_input_is_editable,
- startup_signer_status_spec, startup_signer_transport_failure_requires_notice,
+ AppTextKey, FarmerHomeFarmState, HomeStage, SETTINGS_FARM_PANEL_SECTIONS,
+ SETTINGS_NAVIGATION_ORDER, SETTINGS_OPERATIONS_PANEL_SECTIONS,
+ SettingsInventorySectionSpec, SettingsPanelViewKey, StartupHomeSurface,
+ StartupSignerConnectState, farm_setup_onboarding_card_spec, farmer_home_farm_state,
+ farmer_pack_day_available, home_content_scroll_id, home_saved_farm,
+ home_sidebar_navigation_sections, home_stage, home_window_launch_size_px,
+ home_window_minimum_size_px, parse_optional_product_editor_stock_input,
+ parse_product_editor_price_input, product_display_title, startup_home_surface,
+ startup_signer_preview_summary, startup_signer_preview_summary_for_connect_state,
+ startup_signer_source_input_is_editable, startup_signer_status_spec,
+ startup_signer_transport_failure_requires_notice,
};
use crate::runtime::DesktopAppRuntimeSummary;
use radroots_app_models::SettingsAccountProjection;
use radroots_app_models::{
- AppStartupGate, FarmId, FarmOrderMethod, FarmReadiness, FarmSetupDraft,
+ ActiveSurface, AppStartupGate, FarmId, FarmOrderMethod, FarmReadiness, FarmSetupDraft,
FarmSetupProjection, FarmSummary, FarmerSection, FulfillmentWindowId,
FulfillmentWindowSummary, LoggedOutStartupPhase, LoggedOutStartupProjection,
- PackDayProjection, TodayAgendaProjection, TodaySetupTask, TodaySetupTaskKind,
+ PackDayProjection, PersonalSection, ShellSection, TodayAgendaProjection, TodaySetupTask,
+ TodaySetupTaskKind,
};
use radroots_app_remote_signer::{
RadrootsAppRemoteSignerApprovedSession, RadrootsAppRemoteSignerPendingSession,
@@ -7827,6 +8208,22 @@ mod tests {
}
#[test]
+ fn home_stage_uses_buyer_workspace_when_guest_enters_marketplace() {
+ let mut guest_marketplace = summary(
+ HomeRoute::SetupRequired,
+ TodayAgendaProjection::default(),
+ FarmSetupProjection::default(),
+ );
+ guest_marketplace.startup_gate = AppStartupGate::SetupRequired;
+ guest_marketplace.shell_projection = AppShellProjection::new(
+ ActiveSurface::Personal,
+ ShellSection::Personal(PersonalSection::Browse),
+ );
+
+ assert_eq!(home_stage(&guest_marketplace), HomeStage::BuyerWorkspace);
+ }
+
+ #[test]
fn farmer_home_farm_state_distinguishes_no_farm_incomplete_and_configured() {
let farm_id = FarmId::new();
let incomplete_farm = FarmSummary {
@@ -8158,6 +8555,7 @@ mod tests {
startup_gate: AppStartupGate::Farmer,
logged_out_startup: LoggedOutStartupProjection::default(),
home_route,
+ personal_projection: Default::default(),
farm_readiness_projection,
farm_setup_projection,
today_projection,
diff --git a/crates/shared/i18n/src/keys.rs b/crates/shared/i18n/src/keys.rs
@@ -22,6 +22,13 @@ macro_rules! define_app_text_keys {
define_app_text_keys! {
AppName => "app.name",
HomeBrand => "home.brand",
+ HomeHeaderMarketplaceMode => "home.header.marketplace_mode",
+ HomeHeaderFarmMode => "home.header.farm_mode",
+ HomeHeaderAccountSetupAction => "home.header.account_setup_action",
+ HomeHeaderGuestLabel => "home.header.guest_label",
+ HomeNavBrowse => "home.nav.browse",
+ HomeNavSearch => "home.nav.search",
+ HomeNavCart => "home.nav.cart",
HomeNavToday => "home.nav.today",
HomeNavProducts => "home.nav.products",
HomeNavOrders => "home.nav.orders",
@@ -53,6 +60,7 @@ define_app_text_keys! {
HomeSetupStarting => "home.setup.starting",
HomeSetupCreateAccountAction => "home.setup.create_account",
HomeSetupContinueAction => "home.setup.continue_action",
+ HomeSetupBrowseMarketplaceAction => "home.setup.browse_marketplace_action",
HomeSetupGenerateKeyAction => "home.setup.generate_key_action",
HomeSetupConnectSignerAction => "home.setup.connect_signer_action",
HomeSetupSignerSourcePlaceholder => "home.setup.signer_source.placeholder",
@@ -92,6 +100,10 @@ define_app_text_keys! {
HomeTodayEmptyNoFarmBody => "home.today.empty.no_farm.body",
HomeTodayEmptyQuietTitle => "home.today.empty.quiet.title",
HomeTodayEmptyQuietBody => "home.today.empty.quiet.body",
+ PersonalBrowsePlaceholderBody => "personal.browse.placeholder.body",
+ PersonalSearchPlaceholderBody => "personal.search.placeholder.body",
+ PersonalCartPlaceholderBody => "personal.cart.placeholder.body",
+ PersonalOrdersPlaceholderBody => "personal.orders.placeholder.body",
OrdersTitle => "orders.title",
OrdersFiltersTitle => "orders.filters.title",
OrdersSummaryTotal => "orders.summary.total",
diff --git a/crates/shared/state/src/lib.rs b/crates/shared/state/src/lib.rs
@@ -524,6 +524,7 @@ pub enum AppStateCommand {
SetStartupSignerSourceInput(String),
ResetLoggedOutStartup,
ReplaceIdentityProjection(AppIdentityProjection),
+ ReplacePersonalProjection(PersonalWorkspaceProjection),
ReplaceFarmSetupProjection(FarmSetupProjection),
ReplaceFarmRulesProjection(FarmRulesProjection),
SelectFarmSetupFlowStage(FarmSetupFlowStage),
@@ -584,6 +585,10 @@ impl AppStateCommand {
Self::ReplaceIdentityProjection(projection)
}
+ pub fn replace_personal_projection(projection: PersonalWorkspaceProjection) -> Self {
+ Self::ReplacePersonalProjection(projection)
+ }
+
pub fn replace_farm_setup_projection(projection: FarmSetupProjection) -> Self {
Self::ReplaceFarmSetupProjection(projection)
}
@@ -998,6 +1003,9 @@ fn apply_command(projection: &mut AppProjection, command: AppStateCommand) -> Ap
AppStateCommand::ReplaceIdentityProjection(identity_projection) => {
projection.identity = identity_projection;
}
+ AppStateCommand::ReplacePersonalProjection(personal_projection) => {
+ projection.personal = personal_projection;
+ }
AppStateCommand::ReplaceFarmSetupProjection(farm_setup_projection) => {
projection.farm_setup = farm_setup_projection;
}
diff --git a/i18n/locales/en/messages.json b/i18n/locales/en/messages.json
@@ -1,6 +1,13 @@
{
"app.name": "Radroots",
"home.brand": "radroots",
+ "home.header.marketplace_mode": "Marketplace",
+ "home.header.farm_mode": "Farm",
+ "home.header.account_setup_action": "Set up account",
+ "home.header.guest_label": "Guest",
+ "home.nav.browse": "Browse",
+ "home.nav.search": "Search",
+ "home.nav.cart": "Cart",
"home.nav.today": "Today",
"home.nav.products": "Products",
"home.nav.orders": "Orders",
@@ -32,6 +39,7 @@
"home.setup.starting": "Starting...",
"home.setup.create_account": "Create account",
"home.setup.continue_action": "Continue",
+ "home.setup.browse_marketplace_action": "Browse marketplace",
"home.setup.generate_key_action": "Generate key",
"home.setup.connect_signer_action": "Connect signer",
"home.setup.signer_source.placeholder": "Paste bunker URI or discovery URL",
@@ -71,6 +79,10 @@
"home.today.empty.no_farm.body": "Create a farm to start using the farmer workspace.",
"home.today.empty.quiet.title": "Nothing urgent right now",
"home.today.empty.quiet.body": "Orders, stock, and drafts will appear here when they need attention.",
+ "personal.browse.placeholder.body": "Products from local farms will appear here when they are available.",
+ "personal.search.placeholder.body": "Search will use the same marketplace listings and stay focused on products, farms, and pickup options.",
+ "personal.cart.placeholder.body": "Add items from one farm to start an order.",
+ "personal.orders.placeholder.body": "Placed orders will appear here on this device.",
"orders.title": "Orders",
"orders.filters.title": "View",
"orders.summary.total": "Total orders",