//! Inventory popup component. use leptos::prelude::*; use leptos::reactive::owner::LocalStorage; use uuid::Uuid; use chattyness_db::models::{InventoryItem, PublicProp}; #[cfg(feature = "hydrate")] use chattyness_db::ws_messages::ClientMessage; use super::modals::Modal; use super::tabs::{Tab, TabBar}; use super::ws_client::WsSender; /// Inventory popup component. /// /// Shows a tabbed interface with: /// - My Inventory: User-owned props with drop functionality /// - Server: Public server-wide props /// - Realm: Public realm-specific props /// /// Props: /// - `open`: Signal controlling visibility /// - `on_close`: Callback when popup should close /// - `ws_sender`: WebSocket sender for dropping props /// - `realm_slug`: Current realm slug for fetching realm props #[component] pub fn InventoryPopup( #[prop(into)] open: Signal, on_close: Callback<()>, ws_sender: StoredValue, LocalStorage>, #[prop(into)] realm_slug: Signal, ) -> impl IntoView { // Tab state let (active_tab, set_active_tab) = signal("my_inventory"); // My Inventory state let (items, set_items) = signal(Vec::::new()); let (loading, set_loading) = signal(false); let (error, set_error) = signal(Option::::None); let (selected_item, set_selected_item) = signal(Option::::None); let (dropping, set_dropping) = signal(false); // Server props state let (server_props, set_server_props) = signal(Vec::::new()); let (server_loading, set_server_loading) = signal(false); let (server_error, set_server_error) = signal(Option::::None); // Realm props state let (realm_props, set_realm_props) = signal(Vec::::new()); let (realm_loading, set_realm_loading) = signal(false); let (realm_error, set_realm_error) = signal(Option::::None); // Track if tabs have been loaded (to avoid re-fetching) let (my_inventory_loaded, set_my_inventory_loaded) = signal(false); let (server_loaded, set_server_loaded) = signal(false); let (realm_loaded, set_realm_loaded) = signal(false); // Fetch my inventory when popup opens or tab is selected #[cfg(feature = "hydrate")] { use gloo_net::http::Request; use leptos::task::spawn_local; Effect::new(move |_| { if !open.get() { // Reset state when closing set_selected_item.set(None); set_my_inventory_loaded.set(false); set_server_loaded.set(false); set_realm_loaded.set(false); return; } // Only fetch if on my_inventory tab and not already loaded if active_tab.get() != "my_inventory" || my_inventory_loaded.get() { return; } set_loading.set(true); set_error.set(None); spawn_local(async move { let response = Request::get("/api/inventory").send().await; match response { Ok(resp) if resp.ok() => { if let Ok(data) = resp .json::() .await { set_items.set(data.items); set_my_inventory_loaded.set(true); } else { set_error.set(Some("Failed to parse inventory data".to_string())); } } Ok(resp) => { set_error.set(Some(format!("Failed to load inventory: {}", resp.status()))); } Err(e) => { set_error.set(Some(format!("Network error: {}", e))); } } set_loading.set(false); }); }); } // Fetch server props when server tab is selected #[cfg(feature = "hydrate")] { use gloo_net::http::Request; use leptos::task::spawn_local; Effect::new(move |_| { if !open.get() || active_tab.get() != "server" || server_loaded.get() { return; } set_server_loading.set(true); set_server_error.set(None); spawn_local(async move { let response = Request::get("/api/inventory/server").send().await; match response { Ok(resp) if resp.ok() => { if let Ok(data) = resp .json::() .await { set_server_props.set(data.props); set_server_loaded.set(true); } else { set_server_error.set(Some("Failed to parse server props".to_string())); } } Ok(resp) => { set_server_error.set(Some(format!( "Failed to load server props: {}", resp.status() ))); } Err(e) => { set_server_error.set(Some(format!("Network error: {}", e))); } } set_server_loading.set(false); }); }); } // Fetch realm props when realm tab is selected #[cfg(feature = "hydrate")] { use gloo_net::http::Request; use leptos::task::spawn_local; Effect::new(move |_| { if !open.get() || active_tab.get() != "realm" || realm_loaded.get() { return; } let slug = realm_slug.get(); if slug.is_empty() { set_realm_error.set(Some("No realm selected".to_string())); return; } set_realm_loading.set(true); set_realm_error.set(None); spawn_local(async move { let response = Request::get(&format!("/api/realms/{}/inventory", slug)) .send() .await; match response { Ok(resp) if resp.ok() => { if let Ok(data) = resp .json::() .await { set_realm_props.set(data.props); set_realm_loaded.set(true); } else { set_realm_error.set(Some("Failed to parse realm props".to_string())); } } Ok(resp) => { set_realm_error.set(Some(format!( "Failed to load realm props: {}", resp.status() ))); } Err(e) => { set_realm_error.set(Some(format!("Network error: {}", e))); } } set_realm_loading.set(false); }); }); } // Handle drop action via WebSocket #[cfg(feature = "hydrate")] let handle_drop = { move |item_id: Uuid| { set_dropping.set(true); // Send drop command via WebSocket ws_sender.with_value(|sender| { if let Some(send_fn) = sender { send_fn(ClientMessage::DropProp { inventory_item_id: item_id, }); // Optimistically remove from local list set_items.update(|items| { items.retain(|i| i.id != item_id); }); set_selected_item.set(None); } else { set_error.set(Some("Not connected to server".to_string())); } }); set_dropping.set(false); } }; #[cfg(not(feature = "hydrate"))] let handle_drop = |_item_id: Uuid| {}; view! { // Tab bar // Tab content
// My Inventory tab // Server tab // Realm tab
} } /// My Inventory tab content with drop functionality. #[component] fn MyInventoryTab( #[prop(into)] items: Signal>, #[prop(into)] loading: Signal, #[prop(into)] error: Signal>, #[prop(into)] selected_item: Signal>, set_selected_item: WriteSignal>, #[prop(into)] dropping: Signal, #[prop(into)] on_drop: Callback, ) -> impl IntoView { view! { // Loading state

"Loading inventory..."

// Error state

{move || error.get().unwrap_or_default()}

// Empty state

"Your inventory is empty"

"Collect props to see them here"

// Grid of items
} } />
// Selected item details and actions {move || { let item_id = selected_item.get()?; let item = items.get().into_iter().find(|i| i.id == item_id)?; let on_drop = on_drop.clone(); let is_dropping = dropping.get(); let is_droppable = item.is_droppable; Some(view! {

{item.prop_name.clone()}

{if item.is_transferable { "Transferable" } else { "Not transferable" }} {if item.is_portable { " \u{2022} Portable" } else { "" }} {if is_droppable { " \u{2022} Droppable" } else { " \u{2022} Essential" }}

// Drop button - disabled for non-droppable (essential) props // Transfer button (disabled for now)
}) }}
} } /// Public props tab content (read-only display). #[component] fn PublicPropsTab( #[prop(into)] props: Signal>, #[prop(into)] loading: Signal, #[prop(into)] error: Signal>, tab_name: &'static str, empty_message: &'static str, ) -> impl IntoView { // Selected prop for showing details let (selected_prop, set_selected_prop) = signal(Option::::None); view! { // Loading state

{format!("Loading {} props...", tab_name.to_lowercase())}

// Error state

{move || error.get().unwrap_or_default()}

// Empty state

{empty_message}

"Public props will appear here when available"

// Grid of props
} } />
// Selected prop details (read-only) {move || { let prop_id = selected_prop.get()?; let prop = props.get().into_iter().find(|p| p.id == prop_id)?; Some(view! {

{prop.name.clone()}

{prop.description.map(|desc| view! {

{desc}

})}

"View only"

}) }}
} }