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
This commit is contained in:
@@ -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};
|
||||
|
||||
@@ -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,
|
||||
"📋 <b>My Listings</b>\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!(
|
||||
"📋 <b>My Listings</b>\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::<i64>()?);
|
||||
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?;
|
||||
|
||||
@@ -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!(
|
||||
"✅ <b>{} selected!</b>\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<MessageTarget>,
|
||||
) -> 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<MessageTarget>,
|
||||
) -> 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<MessageTarget>,
|
||||
) -> HandlerResult {
|
||||
let target = target.into();
|
||||
let button = DurationKeyboardButtons::try_from(data).unwrap();
|
||||
let duration = ListingDuration::days(match button {
|
||||
DurationKeyboardButtons::OneDay => 1,
|
||||
DurationKeyboardButtons::ThreeDays => 3,
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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<MessageTarget>,
|
||||
) -> 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!(
|
||||
"🛍️ <b>Creating New Fixed Price Listing</b>\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)?;
|
||||
|
||||
@@ -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"),]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
"🛍️ <b>What type of listing would you like to create?</b>\n\n\
|
||||
<b>🛍️ Fixed Price:</b> Set a fixed price for immediate purchase\n\
|
||||
<b>⏰ Basic Auction:</b> Traditional time-based auction with bidding\n\
|
||||
<b>🎭 Blind Auction:</b> Buyers submit sealed bids, you choose the winner\n\
|
||||
<b>🎯 Multi-Slot Auction:</b> 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()
|
||||
}
|
||||
|
||||
@@ -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::*;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 {
|
||||
"🎯 <b>Welcome to Pawctioneer Bot!</b> 🎯\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<MessageTarget>,
|
||||
) -> 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,
|
||||
"💰 <b>My Bids (Coming Soon)</b>\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,
|
||||
"⚙️ <b>Settings (Coming Soon)</b>\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!(
|
||||
"📋 <b>Available Commands:</b>\n\n{}\n\n\
|
||||
📧 <b>Support:</b> Contact @admin for help\n\
|
||||
🔗 <b>More info:</b> 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(())
|
||||
}
|
||||
|
||||
@@ -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<Self, Self::Error> {
|
||||
match value {
|
||||
$($(
|
||||
$callback_data => Ok(Self::$variant),
|
||||
)*)*
|
||||
_ => Err(value),
|
||||
_ => anyhow::bail!("Unknown {name} button: {value}", name = stringify!($name)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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::<Update, SqliteStorage<Json>, 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()
|
||||
|
||||
Reference in New Issue
Block a user