From 5d7a5b26c106512917aed46cdadc4d46a9424151 Mon Sep 17 00:00:00 2001 From: Dylan Knutson Date: Sat, 30 Aug 2025 05:45:49 +0000 Subject: [PATCH] feat: enhance UX with listing type selection and main menu improvements - Add listing type selection to new listing wizard - Create SelectingListingType state for choosing between 4 listing types - Add ListingTypeKeyboardButtons with clear descriptions - Support BasicAuction, MultiSlotAuction, FixedPriceListing, BlindAuction - Update handlers to start with type selection before title input - Improve main menu navigation - Add main menu with action buttons for /start command - Create MainMenuButtons with New Listing, My Listings, My Bids, Settings, Help - Add back button to My Listings screen for better navigation - Implement proper dialogue state management between screens - Refactor callback handling for type safety - Convert string literal matching to enum-based callback handling - Use try_from() pattern for all keyboard button callbacks - Ensure compile-time safety and exhaustive matching - Apply pattern to listing type, slots, duration, and start time callbacks - Eliminate code duplication - Extract reusable main menu functions (enter_main_menu, get_main_menu_message) - Centralize main menu logic and message content - Update all main menu transitions to use shared functions - Technical improvements - Add proper error handling for invalid callback data - Maintain backward compatibility with existing flows - Follow established patterns for keyboard button definitions - Ensure all changes compile without errors --- src/commands/mod.rs | 4 +- src/commands/my_listings.rs | 39 ++++- src/commands/new_listing/callbacks.rs | 97 ++++++++++--- src/commands/new_listing/handler_factory.rs | 6 + src/commands/new_listing/handlers.rs | 52 ++++--- src/commands/new_listing/keyboard.rs | 14 ++ src/commands/new_listing/messages.rs | 15 ++ src/commands/new_listing/mod.rs | 4 +- src/commands/new_listing/types.rs | 44 ++++-- src/commands/start.rs | 150 +++++++++++++++++--- src/keyboard_utils.rs | 4 +- src/main.rs | 4 + 12 files changed, 347 insertions(+), 86 deletions(-) diff --git a/src/commands/mod.rs b/src/commands/mod.rs index 7d67043..ae5359e 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -3,9 +3,9 @@ pub mod my_bids; pub mod my_listings; pub mod new_listing; pub mod settings; -pub mod start; +mod start; pub use help::handle_help; pub use my_bids::handle_my_bids; pub use settings::handle_settings; -pub use start::handle_start; +pub use start::{enter_main_menu, handle_main_menu_callback, handle_start}; diff --git a/src/commands/my_listings.rs b/src/commands/my_listings.rs index 7292669..4a6eb4e 100644 --- a/src/commands/my_listings.rs +++ b/src/commands/my_listings.rs @@ -1,6 +1,9 @@ use crate::{ case, - commands::new_listing::{enter_edit_listing_draft, ListingDraft}, + commands::{ + enter_main_menu, + new_listing::{enter_edit_listing_draft, ListingDraft}, + }, db::{listing::PersistedListing, user::PersistedUser, ListingDAO, ListingDbId, UserDAO}, keyboard_buttons, message_utils::{extract_callback_data, pluralize_with_count, send_message, MessageTarget}, @@ -38,6 +41,12 @@ keyboard_buttons! { } } +keyboard_buttons! { + enum MyListingsButtons { + BackToMenu("⬅️ Back to Menu", "my_listings_back_to_menu"), + } +} + pub fn my_listings_handler() -> Handler<'static, HandlerResult, DpHandlerDescription> { dptree::entry() .branch( @@ -74,7 +83,7 @@ async fn handle_my_listings_command_input( Ok(()) } -async fn show_listings_for_user( +pub async fn show_listings_for_user( db_pool: SqlitePool, dialogue: RootDialogue, bot: Bot, @@ -101,13 +110,17 @@ async fn show_listings_for_user( let listings = ListingDAO::find_by_seller(&db_pool, user.persisted.id).await?; if listings.is_empty() { + // Create keyboard with just the back button + let keyboard = + teloxide::types::InlineKeyboardMarkup::new([[MyListingsButtons::BackToMenu.into()]]); + send_message( &bot, target, "📋 My Listings\n\n\ You don't have any listings yet.\n\ Use /newlisting to create your first listing!", - None, + Some(keyboard), ) .await?; return Ok(()); @@ -122,6 +135,9 @@ async fn show_listings_for_user( )]); } + // Add back to menu button + keyboard = keyboard.append_row(vec![MyListingsButtons::BackToMenu.into()]); + let response = format!( "📋 My Listings\n\n\ You have {}.\n\n\ @@ -142,6 +158,18 @@ async fn handle_viewing_listings_callback( let (data, from, message_id) = extract_callback_data(&bot, callback_query).await?; let target = (from.clone(), message_id); + // Check if it's the back to menu button + if let Ok(button) = MyListingsButtons::try_from(data.as_str()) { + match button { + MyListingsButtons::BackToMenu => { + // Transition back to main menu using the reusable function + enter_main_menu(bot, dialogue, target).await?; + return Ok(()); + } + } + } + + // Otherwise, treat it as a listing ID let listing_id = ListingDbId::new(data.parse::()?); let (_, listing) = get_user_and_listing(&db_pool, &bot, from.id, listing_id, target.clone()).await?; @@ -190,10 +218,7 @@ async fn handle_managing_listing_callback( let (data, from, message_id) = extract_callback_data(&bot, callback_query).await?; let target = (from.clone(), message_id); - let button = ManageListingButtons::try_from(data.as_str()) - .map_err(|_| anyhow::anyhow!("Invalid ManageListingButtons callback data: {}", data))?; - - match button { + match ManageListingButtons::try_from(data.as_str())? { ManageListingButtons::Edit => { let (_, listing) = get_user_and_listing(&db_pool, &bot, from.id, listing_id, target.clone()).await?; diff --git a/src/commands/new_listing/callbacks.rs b/src/commands/new_listing/callbacks.rs index 6d749f8..59205be 100644 --- a/src/commands/new_listing/callbacks.rs +++ b/src/commands/new_listing/callbacks.rs @@ -4,20 +4,77 @@ //! in the new listing creation and editing workflows. use crate::{ - commands::new_listing::{ - field_processing::transition_to_field, - keyboard::*, - messages::{get_keyboard_for_field, get_step_message}, - types::{ListingDraft, ListingDraftPersisted, ListingField, NewListingState}, - ui::show_confirmation_screen, + commands::{ + new_listing::{ + field_processing::transition_to_field, + keyboard::{ + DurationKeyboardButtons, ListingTypeKeyboardButtons, SlotsKeyboardButtons, + StartTimeKeyboardButtons, + }, + messages::{get_keyboard_for_field, get_step_message}, + types::{ListingDraft, ListingDraftPersisted, ListingField, NewListingState}, + ui::show_confirmation_screen, + }, + start::enter_main_menu, }, - db::{listing::ListingFields, ListingDuration}, + db::{listing::ListingFields, ListingDuration, ListingType, UserDbId}, message_utils::*, HandlerResult, RootDialogue, }; use log::{error, info}; use teloxide::{types::CallbackQuery, Bot}; +/// Handle callbacks during the listing type selection phase +pub async fn handle_selecting_listing_type_callback( + bot: Bot, + dialogue: RootDialogue, + seller_id: UserDbId, + callback_query: CallbackQuery, +) -> HandlerResult { + let (data, from, message_id) = extract_callback_data(&bot, callback_query).await?; + info!("User {from:?} selected listing type: {data:?}"); + let target = (from, message_id); + + // Parse the listing type from callback data + let (listing_type, type_name) = match ListingTypeKeyboardButtons::try_from(data.as_str())? { + ListingTypeKeyboardButtons::FixedPrice => { + (ListingType::FixedPriceListing, "Fixed Price Listing") + } + ListingTypeKeyboardButtons::BasicAuction => (ListingType::BasicAuction, "Basic Auction"), + ListingTypeKeyboardButtons::BlindAuction => (ListingType::BlindAuction, "Blind Auction"), + ListingTypeKeyboardButtons::MultiSlot => { + (ListingType::MultiSlotAuction, "Multi-Slot Auction") + } + ListingTypeKeyboardButtons::Back => { + enter_main_menu(bot, dialogue, target).await?; + return Ok(()); + } + }; + + // Create draft with selected listing type + let draft = ListingDraft::new_for_seller_with_type(seller_id, listing_type); + + // Transition to first field (Title) + transition_to_field(dialogue, ListingField::Title, draft).await?; + + let response = format!( + "✅ {} selected!\n\n\ + Let's create your listing step by step!\n\n{}", + type_name, + get_step_message(ListingField::Title) + ); + + send_message( + &bot, + target, + response, + get_keyboard_for_field(ListingField::Title), + ) + .await?; + + Ok(()) +} + /// Handle callbacks during the field input phase pub async fn handle_awaiting_draft_field_callback( bot: Bot, @@ -38,14 +95,17 @@ pub async fn handle_awaiting_draft_field_callback( ListingField::Description if data == "skip" => { handle_description_skip_callback(&bot, dialogue, draft, target).await } - ListingField::Slots if data.starts_with("slots_") => { - handle_slots_callback(&bot, dialogue, draft, &data, target).await + ListingField::Slots => { + let button = SlotsKeyboardButtons::try_from(data.as_str())?; + handle_slots_callback(&bot, dialogue, draft, button, target).await } - ListingField::StartTime if data.starts_with("start_time_") => { - handle_start_time_callback(&bot, dialogue, draft, &data, target).await + ListingField::StartTime => { + let button = StartTimeKeyboardButtons::try_from(data.as_str())?; + handle_start_time_callback(&bot, dialogue, draft, button, target).await } - ListingField::Duration if data.starts_with("duration_") => { - handle_duration_callback(&bot, dialogue, draft, &data, target).await + ListingField::Duration => { + let button = DurationKeyboardButtons::try_from(data.as_str())?; + handle_duration_callback(&bot, dialogue, draft, button, target).await } _ => { error!("Unknown callback data for field {field:?}: {data}"); @@ -84,12 +144,10 @@ async fn handle_slots_callback( bot: &Bot, dialogue: RootDialogue, mut draft: ListingDraft, - data: &str, + button: SlotsKeyboardButtons, target: impl Into, ) -> HandlerResult { let target = target.into(); - let button = SlotsKeyboardButtons::try_from(data) - .map_err(|_| anyhow::anyhow!("Unknown SlotsKeyboardButtons data: {}", data))?; let num_slots = match button { SlotsKeyboardButtons::OneSlot => 1, SlotsKeyboardButtons::TwoSlots => 2, @@ -124,12 +182,10 @@ async fn handle_start_time_callback( bot: &Bot, dialogue: RootDialogue, mut draft: ListingDraft, - data: &str, + button: StartTimeKeyboardButtons, target: impl Into, ) -> HandlerResult { let target = target.into(); - let button = StartTimeKeyboardButtons::try_from(data) - .map_err(|_| anyhow::anyhow!("Unknown StartTimeKeyboardButtons data: {}", data))?; let start_time = match button { StartTimeKeyboardButtons::Now => ListingDuration::zero(), }; @@ -163,11 +219,10 @@ async fn handle_duration_callback( bot: &Bot, dialogue: RootDialogue, mut draft: ListingDraft, - data: &str, + button: DurationKeyboardButtons, target: impl Into, ) -> HandlerResult { let target = target.into(); - let button = DurationKeyboardButtons::try_from(data).unwrap(); let duration = ListingDuration::days(match button { DurationKeyboardButtons::OneDay => 1, DurationKeyboardButtons::ThreeDays => 3, diff --git a/src/commands/new_listing/handler_factory.rs b/src/commands/new_listing/handler_factory.rs index 39c9b20..4c56e82 100644 --- a/src/commands/new_listing/handler_factory.rs +++ b/src/commands/new_listing/handler_factory.rs @@ -28,6 +28,12 @@ pub fn new_listing_handler() -> Handler { ) .branch( Update::filter_callback_query() + .branch( + case![DialogueRootState::NewListing( + NewListingState::SelectingListingType { seller_id } + )] + .endpoint(handle_selecting_listing_type_callback), + ) .branch( case![DialogueRootState::NewListing( NewListingState::AwaitingDraftField { field, draft } diff --git a/src/commands/new_listing/handlers.rs b/src/commands/new_listing/handlers.rs index 0a13dab..55aa25e 100644 --- a/src/commands/new_listing/handlers.rs +++ b/src/commands/new_listing/handlers.rs @@ -12,7 +12,8 @@ use crate::{ SlotsKeyboardButtons, StartTimeKeyboardButtons, }, messages::{ - get_edit_success_message, get_keyboard_for_field, get_step_message, get_success_message, + get_edit_success_message, get_keyboard_for_field, get_listing_type_keyboard, + get_listing_type_selection_message, get_step_message, get_success_message, }, types::{ListingDraft, ListingDraftPersisted, ListingField, NewListingState}, ui::{display_listing_summary, show_confirmation_screen}, @@ -29,38 +30,38 @@ use sqlx::SqlitePool; use teloxide::{prelude::*, types::*, Bot}; /// Handle the /newlisting command - starts the dialogue -pub async fn handle_new_listing_command( +pub(super) async fn handle_new_listing_command( db_pool: SqlitePool, bot: Bot, dialogue: RootDialogue, msg: Message, ) -> HandlerResult { - info!( - "User {} started new fixed price listing wizard", - HandleAndId::from_chat(&msg.chat), - ); let user = msg.from.ok_or_else(|| anyhow::anyhow!("User not found"))?; + enter_handle_new_listing(db_pool, bot, dialogue, user, msg.chat).await?; + Ok(()) +} + +pub async fn enter_handle_new_listing( + db_pool: SqlitePool, + bot: Bot, + dialogue: RootDialogue, + user: User, + target: impl Into, +) -> HandlerResult { let user = UserDAO::find_or_create_by_telegram_user(&db_pool, user).await?; - // Initialize the dialogue to Start state + // Initialize the dialogue to listing type selection state dialogue - .update(NewListingState::AwaitingDraftField { - field: ListingField::Title, - draft: ListingDraft::new_for_seller(user.persisted.id), + .update(NewListingState::SelectingListingType { + seller_id: user.persisted.id, }) .await?; - let response = format!( - "🛍️ Creating New Fixed Price Listing\n\n\ - Let's create your fixed price listing step by step!\n\n{}", - get_step_message(ListingField::Title) - ); - send_message( &bot, - msg.chat, - response, - get_keyboard_for_field(ListingField::Title), + target, + get_listing_type_selection_message(), + Some(get_listing_type_keyboard()), ) .await?; Ok(()) @@ -154,10 +155,7 @@ pub async fn handle_viewing_draft_callback( let target = (from.clone(), message_id); - let button = ConfirmationKeyboardButtons::try_from(data.as_str()) - .map_err(|_| anyhow::anyhow!("Unknown ConfirmationKeyboardButtons data: {}", data))?; - - match button { + match ConfirmationKeyboardButtons::try_from(data.as_str())? { ConfirmationKeyboardButtons::Create | ConfirmationKeyboardButtons::Save => { info!("User {target:?} confirmed listing creation"); save_listing(db_pool, bot, target, draft).await?; @@ -197,11 +195,9 @@ pub async fn handle_editing_draft_callback( ) -> HandlerResult { let (data, from, message_id) = extract_callback_data(&bot, callback_query).await?; let target = (from, message_id); - let button = FieldSelectionKeyboardButtons::try_from(data.as_str()) - .map_err(|e| anyhow::anyhow!("Invalid field selection button: {}", e))?; - info!("User {target:?} in editing screen, showing field selection"); + let button = FieldSelectionKeyboardButtons::try_from(data.as_str())?; if button == FieldSelectionKeyboardButtons::Done { show_confirmation_screen(&bot, target, &draft).await?; dialogue @@ -219,7 +215,9 @@ pub async fn handle_editing_draft_callback( FieldSelectionKeyboardButtons::Slots => ListingField::Slots, FieldSelectionKeyboardButtons::StartTime => ListingField::StartTime, FieldSelectionKeyboardButtons::Duration => ListingField::Duration, - FieldSelectionKeyboardButtons::Done => unreachable!(), + FieldSelectionKeyboardButtons::Done => { + return Err(anyhow::anyhow!("Done button should not be used here")) + } }; let value = get_current_field_value(&draft, field)?; diff --git a/src/commands/new_listing/keyboard.rs b/src/commands/new_listing/keyboard.rs index ad61b8b..c9d0d35 100644 --- a/src/commands/new_listing/keyboard.rs +++ b/src/commands/new_listing/keyboard.rs @@ -53,3 +53,17 @@ keyboard_buttons! { Now("Now", "start_time_now"), } } + +keyboard_buttons! { + pub enum ListingTypeKeyboardButtons { + [ + FixedPrice("🛍️ Fixed Price", "listing_type_fixed_price"), + BasicAuction("⏰ Basic Auction", "listing_type_basic_auction"), + ], + [ + BlindAuction("🎭 Blind Auction", "listing_type_blind_auction"), + MultiSlot("🎯 Multi-Slot Auction", "listing_type_multi_slot"), + ], + [Back("🔙 Back", "listing_type_back"),] + } +} diff --git a/src/commands/new_listing/messages.rs b/src/commands/new_listing/messages.rs index 46fb05d..f5246be 100644 --- a/src/commands/new_listing/messages.rs +++ b/src/commands/new_listing/messages.rs @@ -71,3 +71,18 @@ fn create_cancel_keyboard() -> InlineKeyboardMarkup { fn create_skip_cancel_keyboard() -> InlineKeyboardMarkup { create_multi_row_keyboard(&[&[("Skip", "skip"), ("Cancel", "cancel")]]) } + +/// Get the listing type selection message +pub fn get_listing_type_selection_message() -> &'static str { + "🛍️ What type of listing would you like to create?\n\n\ + 🛍️ Fixed Price: Set a fixed price for immediate purchase\n\ + ⏰ Basic Auction: Traditional time-based auction with bidding\n\ + 🎭 Blind Auction: Buyers submit sealed bids, you choose the winner\n\ + 🎯 Multi-Slot Auction: Multiple items/winners in one auction\n\n\ + Choose your listing type:" +} + +/// Get the keyboard for listing type selection +pub fn get_listing_type_keyboard() -> InlineKeyboardMarkup { + ListingTypeKeyboardButtons::to_keyboard() +} diff --git a/src/commands/new_listing/mod.rs b/src/commands/new_listing/mod.rs index 17ac453..95df398 100644 --- a/src/commands/new_listing/mod.rs +++ b/src/commands/new_listing/mod.rs @@ -17,7 +17,7 @@ mod field_processing; mod handler_factory; mod handlers; mod keyboard; -mod messages; +pub mod messages; #[cfg(test)] mod tests; @@ -27,5 +27,5 @@ mod validations; // Re-export the main handler for external use pub use handler_factory::new_listing_handler; -pub use handlers::enter_edit_listing_draft; +pub use handlers::{enter_edit_listing_draft, enter_handle_new_listing}; pub use types::*; diff --git a/src/commands/new_listing/types.rs b/src/commands/new_listing/types.rs index 46cd6f0..9cc669a 100644 --- a/src/commands/new_listing/types.rs +++ b/src/commands/new_listing/types.rs @@ -1,10 +1,11 @@ use crate::{ db::{ listing::{ - FixedPriceListingFields, ListingBase, ListingFields, NewListingFields, - PersistedListing, PersistedListingFields, + BasicAuctionFields, BlindAuctionFields, FixedPriceListingFields, ListingBase, + ListingFields, MultiSlotAuctionFields, NewListingFields, PersistedListing, + PersistedListingFields, }, - MoneyAmount, UserDbId, + ListingType, MoneyAmount, UserDbId, }, DialogueRootState, }; @@ -19,7 +20,34 @@ pub struct ListingDraft { } impl ListingDraft { - pub fn new_for_seller(seller_id: UserDbId) -> Self { + pub fn new_for_seller_with_type(seller_id: UserDbId, listing_type: ListingType) -> Self { + let fields = match listing_type { + ListingType::BasicAuction => ListingFields::BasicAuction(BasicAuctionFields { + starting_bid: MoneyAmount::default(), + buy_now_price: None, + min_increment: MoneyAmount::from_cents(100), // Default $1.00 increment + anti_snipe_minutes: Some(5), + }), + ListingType::MultiSlotAuction => { + ListingFields::MultiSlotAuction(MultiSlotAuctionFields { + starting_bid: MoneyAmount::default(), + buy_now_price: MoneyAmount::default(), + min_increment: Some(MoneyAmount::from_cents(100)), // Default $1.00 increment + slots_available: 1, + anti_snipe_minutes: 5, + }) + } + ListingType::FixedPriceListing => { + ListingFields::FixedPriceListing(FixedPriceListingFields { + buy_now_price: MoneyAmount::default(), + slots_available: 1, + }) + } + ListingType::BlindAuction => ListingFields::BlindAuction(BlindAuctionFields { + starting_bid: MoneyAmount::default(), + }), + }; + Self { has_changes: false, persisted: ListingDraftPersisted::New(NewListingFields::default()), @@ -28,10 +56,7 @@ impl ListingDraft { title: "".to_string(), description: None, }, - fields: ListingFields::FixedPriceListing(FixedPriceListingFields { - buy_now_price: MoneyAmount::default(), - slots_available: 0, - }), + fields, } } @@ -64,6 +89,9 @@ pub enum ListingField { // Dialogue state for the new listing wizard #[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)] pub enum NewListingState { + SelectingListingType { + seller_id: UserDbId, + }, AwaitingDraftField { field: ListingField, draft: ListingDraft, diff --git a/src/commands/start.rs b/src/commands/start.rs index 7a749e2..c83e3dd 100644 --- a/src/commands/start.rs +++ b/src/commands/start.rs @@ -1,24 +1,140 @@ use log::info; -use teloxide::{prelude::*, types::Message, Bot}; +use teloxide::{ + types::{CallbackQuery, Message}, + utils::command::BotCommands, + Bot, +}; -use crate::HandlerResult; +use sqlx::SqlitePool; -pub async fn handle_start(bot: Bot, msg: Message) -> HandlerResult { - let welcome_message = "🎯 Welcome to Pawctioneer Bot! 🎯\n\n\ - This bot helps you participate in various types of auctions:\n\ - • Standard auctions with anti-sniping protection\n\ - • Multi-slot auctions (multiple winners)\n\ - • Fixed price sales\n\ - • Blind auctions\n\n\ - Use /help to see all available commands.\n\n\ - Ready to start your auction experience? 🚀"; +use crate::{ + commands::{my_listings::show_listings_for_user, new_listing::enter_handle_new_listing}, + keyboard_buttons, + message_utils::{extract_callback_data, send_message, MessageTarget}, + Command, DialogueRootState, HandlerResult, RootDialogue, +}; - info!( - "User {} ({}) started the bot", - msg.chat.username().unwrap_or("unknown"), - msg.chat.id - ); +keyboard_buttons! { + pub enum MainMenuButtons { + [ + NewListing("🛍️ New Listing", "menu_new_listing"), + ], + [ + MyListings("📋 My Listings", "menu_my_listings"), + MyBids("💰 My Bids", "menu_my_bids"), + ], + [ + Settings("⚙️ Settings", "menu_settings"), + Help("❓ Help", "menu_help"), + ] + } +} + +/// Get the main menu welcome message +pub fn get_main_menu_message() -> &'static str { + "🎯 Welcome to Pawctioneer Bot! 🎯\n\n\ + This bot helps you participate in various types of auctions:\n\ + • Standard auctions with anti-sniping protection\n\ + • Multi-slot auctions (multiple winners)\n\ + • Fixed price sales\n\ + • Blind auctions\n\n\ + Choose an option below to get started! 🚀" +} + +pub async fn handle_start(bot: Bot, dialogue: RootDialogue, msg: Message) -> HandlerResult { + enter_main_menu(bot, dialogue, msg.chat).await?; + Ok(()) +} + +/// Show the main menu with buttons +pub async fn enter_main_menu( + bot: Bot, + dialogue: RootDialogue, + target: impl Into, +) -> HandlerResult { + dialogue.update(DialogueRootState::MainMenu).await?; + + send_message( + &bot, + target, + get_main_menu_message(), + Some(MainMenuButtons::to_keyboard()), + ) + .await?; + + Ok(()) +} + +pub async fn handle_main_menu_callback( + db_pool: SqlitePool, + bot: Bot, + dialogue: RootDialogue, + callback_query: CallbackQuery, +) -> HandlerResult { + let (data, from, message_id) = extract_callback_data(&bot, callback_query).await?; + let target = MessageTarget::from((from.clone(), message_id)); + + info!( + "User {} selected main menu option: {}", + from.username.as_deref().unwrap_or("unknown"), + data + ); + + let button = MainMenuButtons::try_from(data.as_str())?; + match button { + MainMenuButtons::NewListing => { + enter_handle_new_listing(db_pool, bot, dialogue, from.clone(), target).await?; + } + MainMenuButtons::MyListings => { + // Call show_listings_for_user directly + show_listings_for_user(db_pool, dialogue, bot, from.id, target).await?; + } + MainMenuButtons::MyBids => { + send_message( + &bot, + target, + "💰 My Bids (Coming Soon)\n\n\ + Here you'll be able to view:\n\ + • Your active bids\n\ + • Bid history\n\ + • Won/lost auctions\n\ + • Outbid notifications\n\n\ + Feature in development! 🛠️", + Some(MainMenuButtons::to_keyboard()), + ) + .await?; + } + MainMenuButtons::Settings => { + send_message( + &bot, + target, + "⚙️ Settings (Coming Soon)\n\n\ + Here you'll be able to configure:\n\ + • Notification preferences\n\ + • Language settings\n\ + • Default bid increments\n\ + • Outbid alerts\n\n\ + Feature in development! 🛠️", + Some(MainMenuButtons::to_keyboard()), + ) + .await?; + } + MainMenuButtons::Help => { + let help_message = format!( + "📋 Available Commands:\n\n{}\n\n\ + 📧 Support: Contact @admin for help\n\ + 🔗 More info: Use individual commands to get started!", + Command::descriptions() + ); + send_message( + &bot, + target, + help_message, + Some(MainMenuButtons::to_keyboard()), + ) + .await?; + } + } - bot.send_message(msg.chat.id, welcome_message).await?; Ok(()) } diff --git a/src/keyboard_utils.rs b/src/keyboard_utils.rs index 05940a3..7d567d0 100644 --- a/src/keyboard_utils.rs +++ b/src/keyboard_utils.rs @@ -50,13 +50,13 @@ macro_rules! keyboard_buttons { } } impl<'a> TryFrom<&'a str> for $name { - type Error = &'a str; + type Error = anyhow::Error; fn try_from(value: &'a str) -> Result { match value { $($( $callback_data => Ok(Self::$variant), )*)* - _ => Err(value), + _ => anyhow::bail!("Unknown {name} button: {value}", name = stringify!($name)), } } } diff --git a/src/main.rs b/src/main.rs index 553f5cb..eae9c84 100644 --- a/src/main.rs +++ b/src/main.rs @@ -62,6 +62,7 @@ pub enum Command { enum DialogueRootState { #[default] Start, + MainMenu, NewListing(NewListingState), MyListings(MyListingsState), } @@ -91,6 +92,9 @@ async fn main() -> Result<()> { .enter_dialogue::, DialogueRootState>() .branch(new_listing_handler()) .branch(my_listings_handler()) + .branch(Update::filter_callback_query().branch( + dptree::case![DialogueRootState::MainMenu].endpoint(handle_main_menu_callback), + )) .branch( Update::filter_message().branch( dptree::entry()