implement marketplace feature wip
This commit is contained in:
		@@ -443,7 +443,7 @@ impl AssetController {
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    // Generate mock assets for testing
 | 
			
		||||
    fn get_mock_assets() -> Vec<Asset> {
 | 
			
		||||
    pub fn get_mock_assets() -> Vec<Asset> {
 | 
			
		||||
        let now = Utc::now();
 | 
			
		||||
        let mut assets = Vec::new();
 | 
			
		||||
        
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										545
									
								
								actix_mvc_app/src/controllers/marketplace.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										545
									
								
								actix_mvc_app/src/controllers/marketplace.rs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,545 @@
 | 
			
		||||
use actix_web::{web, HttpResponse, Result, http};
 | 
			
		||||
use tera::{Context, Tera};
 | 
			
		||||
use chrono::{Utc, Duration};
 | 
			
		||||
use serde::Deserialize;
 | 
			
		||||
use uuid::Uuid;
 | 
			
		||||
 | 
			
		||||
use crate::models::asset::{Asset, AssetType, AssetStatus};
 | 
			
		||||
use crate::models::marketplace::{Listing, ListingStatus, ListingType, Bid, BidStatus, MarketplaceStatistics};
 | 
			
		||||
use crate::controllers::asset::AssetController;
 | 
			
		||||
use crate::utils::render_template;
 | 
			
		||||
 | 
			
		||||
#[derive(Debug, Deserialize)]
 | 
			
		||||
pub struct ListingForm {
 | 
			
		||||
    pub title: String,
 | 
			
		||||
    pub description: String,
 | 
			
		||||
    pub asset_id: String,
 | 
			
		||||
    pub price: f64,
 | 
			
		||||
    pub currency: String,
 | 
			
		||||
    pub listing_type: String,
 | 
			
		||||
    pub duration_days: Option<u32>,
 | 
			
		||||
    pub tags: Option<String>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Debug, Deserialize)]
 | 
			
		||||
pub struct BidForm {
 | 
			
		||||
    pub amount: f64,
 | 
			
		||||
    pub currency: String,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Debug, Deserialize)]
 | 
			
		||||
pub struct PurchaseForm {
 | 
			
		||||
    pub agree_to_terms: bool,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
pub struct MarketplaceController;
 | 
			
		||||
 | 
			
		||||
impl MarketplaceController {
 | 
			
		||||
    // Display the marketplace dashboard
 | 
			
		||||
    pub async fn index(tmpl: web::Data<Tera>) -> Result<HttpResponse> {
 | 
			
		||||
        let mut context = Context::new();
 | 
			
		||||
        
 | 
			
		||||
        let listings = Self::get_mock_listings();
 | 
			
		||||
        let stats = MarketplaceStatistics::new(&listings);
 | 
			
		||||
        
 | 
			
		||||
        // Get featured listings (up to 4)
 | 
			
		||||
        let featured_listings: Vec<&Listing> = listings.iter()
 | 
			
		||||
            .filter(|l| l.featured && l.status == ListingStatus::Active)
 | 
			
		||||
            .take(4)
 | 
			
		||||
            .collect();
 | 
			
		||||
        
 | 
			
		||||
        // Get recent listings (up to 8)
 | 
			
		||||
        let mut recent_listings: Vec<&Listing> = listings.iter()
 | 
			
		||||
            .filter(|l| l.status == ListingStatus::Active)
 | 
			
		||||
            .collect();
 | 
			
		||||
        
 | 
			
		||||
        // Sort by created_at (newest first)
 | 
			
		||||
        recent_listings.sort_by(|a, b| b.created_at.cmp(&a.created_at));
 | 
			
		||||
        let recent_listings = recent_listings.into_iter().take(8).collect::<Vec<_>>();
 | 
			
		||||
        
 | 
			
		||||
        // Get recent sales (up to 5)
 | 
			
		||||
        let mut recent_sales: Vec<&Listing> = listings.iter()
 | 
			
		||||
            .filter(|l| l.status == ListingStatus::Sold)
 | 
			
		||||
            .collect();
 | 
			
		||||
        
 | 
			
		||||
        // Sort by sold_at (newest first)
 | 
			
		||||
        recent_sales.sort_by(|a, b| {
 | 
			
		||||
            let a_sold = a.sold_at.unwrap_or(a.created_at);
 | 
			
		||||
            let b_sold = b.sold_at.unwrap_or(b.created_at);
 | 
			
		||||
            b_sold.cmp(&a_sold)
 | 
			
		||||
        });
 | 
			
		||||
        let recent_sales = recent_sales.into_iter().take(5).collect::<Vec<_>>();
 | 
			
		||||
        
 | 
			
		||||
        // Add data to context
 | 
			
		||||
        context.insert("active_page", &"marketplace");
 | 
			
		||||
        context.insert("stats", &stats);
 | 
			
		||||
        context.insert("featured_listings", &featured_listings);
 | 
			
		||||
        context.insert("recent_listings", &recent_listings);
 | 
			
		||||
        context.insert("recent_sales", &recent_sales);
 | 
			
		||||
        
 | 
			
		||||
        render_template(&tmpl, "marketplace/index.html", &context)
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    // Display all marketplace listings
 | 
			
		||||
    pub async fn list_listings(tmpl: web::Data<Tera>) -> Result<HttpResponse> {
 | 
			
		||||
        let mut context = Context::new();
 | 
			
		||||
        
 | 
			
		||||
        let listings = Self::get_mock_listings();
 | 
			
		||||
        
 | 
			
		||||
        // Filter active listings
 | 
			
		||||
        let active_listings: Vec<&Listing> = listings.iter()
 | 
			
		||||
            .filter(|l| l.status == ListingStatus::Active)
 | 
			
		||||
            .collect();
 | 
			
		||||
        
 | 
			
		||||
        context.insert("active_page", &"marketplace");
 | 
			
		||||
        context.insert("listings", &active_listings);
 | 
			
		||||
        context.insert("listing_types", &[
 | 
			
		||||
            ListingType::FixedPrice.as_str(),
 | 
			
		||||
            ListingType::Auction.as_str(),
 | 
			
		||||
            ListingType::Exchange.as_str(),
 | 
			
		||||
        ]);
 | 
			
		||||
        context.insert("asset_types", &[
 | 
			
		||||
            AssetType::Token.as_str(),
 | 
			
		||||
            AssetType::NFT.as_str(),
 | 
			
		||||
            AssetType::RealEstate.as_str(),
 | 
			
		||||
            AssetType::IntellectualProperty.as_str(),
 | 
			
		||||
            AssetType::PhysicalAsset.as_str(),
 | 
			
		||||
        ]);
 | 
			
		||||
        
 | 
			
		||||
        render_template(&tmpl, "marketplace/listings.html", &context)
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    // Display my listings
 | 
			
		||||
    pub async fn my_listings(tmpl: web::Data<Tera>) -> Result<HttpResponse> {
 | 
			
		||||
        let mut context = Context::new();
 | 
			
		||||
        
 | 
			
		||||
        let listings = Self::get_mock_listings();
 | 
			
		||||
        
 | 
			
		||||
        // Filter by current user (mock user ID)
 | 
			
		||||
        let user_id = "user-123";
 | 
			
		||||
        let my_listings: Vec<&Listing> = listings.iter()
 | 
			
		||||
            .filter(|l| l.seller_id == user_id)
 | 
			
		||||
            .collect();
 | 
			
		||||
        
 | 
			
		||||
        context.insert("active_page", &"marketplace");
 | 
			
		||||
        context.insert("listings", &my_listings);
 | 
			
		||||
        
 | 
			
		||||
        render_template(&tmpl, "marketplace/my_listings.html", &context)
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    // Display listing details
 | 
			
		||||
    pub async fn listing_detail(tmpl: web::Data<Tera>, path: web::Path<String>) -> Result<HttpResponse> {
 | 
			
		||||
        let listing_id = path.into_inner();
 | 
			
		||||
        let mut context = Context::new();
 | 
			
		||||
        
 | 
			
		||||
        let listings = Self::get_mock_listings();
 | 
			
		||||
        
 | 
			
		||||
        // Find the listing
 | 
			
		||||
        let listing = listings.iter().find(|l| l.id == listing_id);
 | 
			
		||||
        
 | 
			
		||||
        if let Some(listing) = listing {
 | 
			
		||||
            // Get similar listings (same asset type, active)
 | 
			
		||||
            let similar_listings: Vec<&Listing> = listings.iter()
 | 
			
		||||
                .filter(|l| l.asset_type == listing.asset_type && 
 | 
			
		||||
                       l.status == ListingStatus::Active && 
 | 
			
		||||
                       l.id != listing.id)
 | 
			
		||||
                .take(4)
 | 
			
		||||
                .collect();
 | 
			
		||||
            
 | 
			
		||||
            context.insert("active_page", &"marketplace");
 | 
			
		||||
            context.insert("listing", listing);
 | 
			
		||||
            context.insert("similar_listings", &similar_listings);
 | 
			
		||||
            
 | 
			
		||||
            // Add current user info for bid/purchase forms
 | 
			
		||||
            let user_id = "user-123";
 | 
			
		||||
            let user_name = "John Doe";
 | 
			
		||||
            context.insert("user_id", &user_id);
 | 
			
		||||
            context.insert("user_name", &user_name);
 | 
			
		||||
            
 | 
			
		||||
            render_template(&tmpl, "marketplace/listing_detail.html", &context)
 | 
			
		||||
        } else {
 | 
			
		||||
            Ok(HttpResponse::NotFound().finish())
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    // Display create listing form
 | 
			
		||||
    pub async fn create_listing_form(tmpl: web::Data<Tera>) -> Result<HttpResponse> {
 | 
			
		||||
        let mut context = Context::new();
 | 
			
		||||
        
 | 
			
		||||
        // Get user's assets for selection
 | 
			
		||||
        let assets = AssetController::get_mock_assets();
 | 
			
		||||
        let user_id = "user-123"; // Mock user ID
 | 
			
		||||
        
 | 
			
		||||
        let user_assets: Vec<&Asset> = assets.iter()
 | 
			
		||||
            .filter(|a| a.owner_id == user_id && a.status == AssetStatus::Active)
 | 
			
		||||
            .collect();
 | 
			
		||||
        
 | 
			
		||||
        context.insert("active_page", &"marketplace");
 | 
			
		||||
        context.insert("assets", &user_assets);
 | 
			
		||||
        context.insert("listing_types", &[
 | 
			
		||||
            ListingType::FixedPrice.as_str(),
 | 
			
		||||
            ListingType::Auction.as_str(),
 | 
			
		||||
            ListingType::Exchange.as_str(),
 | 
			
		||||
        ]);
 | 
			
		||||
        
 | 
			
		||||
        render_template(&tmpl, "marketplace/create_listing.html", &context)
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    // Create a new listing
 | 
			
		||||
    pub async fn create_listing(
 | 
			
		||||
        tmpl: web::Data<Tera>,
 | 
			
		||||
        form: web::Form<ListingForm>,
 | 
			
		||||
    ) -> Result<HttpResponse> {
 | 
			
		||||
        let form = form.into_inner();
 | 
			
		||||
        
 | 
			
		||||
        // Get the asset details
 | 
			
		||||
        let assets = AssetController::get_mock_assets();
 | 
			
		||||
        let asset = assets.iter().find(|a| a.id == form.asset_id);
 | 
			
		||||
        
 | 
			
		||||
        if let Some(asset) = asset {
 | 
			
		||||
            // Process tags
 | 
			
		||||
            let tags = match form.tags {
 | 
			
		||||
                Some(tags_str) => tags_str.split(',')
 | 
			
		||||
                    .map(|s| s.trim().to_string())
 | 
			
		||||
                    .filter(|s| !s.is_empty())
 | 
			
		||||
                    .collect(),
 | 
			
		||||
                None => Vec::new(),
 | 
			
		||||
            };
 | 
			
		||||
            
 | 
			
		||||
            // Calculate expiration date if provided
 | 
			
		||||
            let expires_at = form.duration_days.map(|days| {
 | 
			
		||||
                Utc::now() + Duration::days(days as i64)
 | 
			
		||||
            });
 | 
			
		||||
            
 | 
			
		||||
            // Parse listing type
 | 
			
		||||
            let listing_type = match form.listing_type.as_str() {
 | 
			
		||||
                "Fixed Price" => ListingType::FixedPrice,
 | 
			
		||||
                "Auction" => ListingType::Auction,
 | 
			
		||||
                "Exchange" => ListingType::Exchange,
 | 
			
		||||
                _ => ListingType::FixedPrice,
 | 
			
		||||
            };
 | 
			
		||||
            
 | 
			
		||||
            // Mock user data
 | 
			
		||||
            let user_id = "user-123";
 | 
			
		||||
            let user_name = "John Doe";
 | 
			
		||||
            
 | 
			
		||||
            // Create the listing
 | 
			
		||||
            let _listing = Listing::new(
 | 
			
		||||
                form.title,
 | 
			
		||||
                form.description,
 | 
			
		||||
                asset.id.clone(),
 | 
			
		||||
                asset.name.clone(),
 | 
			
		||||
                asset.asset_type.clone(),
 | 
			
		||||
                user_id.to_string(),
 | 
			
		||||
                user_name.to_string(),
 | 
			
		||||
                form.price,
 | 
			
		||||
                form.currency,
 | 
			
		||||
                listing_type,
 | 
			
		||||
                expires_at,
 | 
			
		||||
                tags,
 | 
			
		||||
                asset.image_url.clone(),
 | 
			
		||||
            );
 | 
			
		||||
            
 | 
			
		||||
            // In a real application, we would save the listing to a database here
 | 
			
		||||
            
 | 
			
		||||
            // Redirect to the marketplace
 | 
			
		||||
            Ok(HttpResponse::SeeOther()
 | 
			
		||||
                .insert_header((http::header::LOCATION, "/marketplace"))
 | 
			
		||||
                .finish())
 | 
			
		||||
        } else {
 | 
			
		||||
            // Asset not found
 | 
			
		||||
            let mut context = Context::new();
 | 
			
		||||
            context.insert("active_page", &"marketplace");
 | 
			
		||||
            context.insert("error", &"Asset not found");
 | 
			
		||||
            
 | 
			
		||||
            render_template(&tmpl, "marketplace/create_listing.html", &context)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    // Submit a bid on an auction listing
 | 
			
		||||
    pub async fn submit_bid(
 | 
			
		||||
        tmpl: web::Data<Tera>,
 | 
			
		||||
        path: web::Path<String>,
 | 
			
		||||
        form: web::Form<BidForm>,
 | 
			
		||||
    ) -> Result<HttpResponse> {
 | 
			
		||||
        let listing_id = path.into_inner();
 | 
			
		||||
        let form = form.into_inner();
 | 
			
		||||
        
 | 
			
		||||
        // In a real application, we would:
 | 
			
		||||
        // 1. Find the listing in the database
 | 
			
		||||
        // 2. Validate the bid
 | 
			
		||||
        // 3. Create the bid
 | 
			
		||||
        // 4. Save it to the database
 | 
			
		||||
        
 | 
			
		||||
        // For now, we'll just redirect back to the listing
 | 
			
		||||
        Ok(HttpResponse::SeeOther()
 | 
			
		||||
            .insert_header((http::header::LOCATION, format!("/marketplace/{}", listing_id)))
 | 
			
		||||
            .finish())
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    // Purchase a fixed-price listing
 | 
			
		||||
    pub async fn purchase_listing(
 | 
			
		||||
        tmpl: web::Data<Tera>,
 | 
			
		||||
        path: web::Path<String>,
 | 
			
		||||
        form: web::Form<PurchaseForm>,
 | 
			
		||||
    ) -> Result<HttpResponse> {
 | 
			
		||||
        let listing_id = path.into_inner();
 | 
			
		||||
        let form = form.into_inner();
 | 
			
		||||
        
 | 
			
		||||
        if !form.agree_to_terms {
 | 
			
		||||
            // User must agree to terms
 | 
			
		||||
            return Ok(HttpResponse::SeeOther()
 | 
			
		||||
                .insert_header((http::header::LOCATION, format!("/marketplace/{}", listing_id)))
 | 
			
		||||
                .finish());
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        // In a real application, we would:
 | 
			
		||||
        // 1. Find the listing in the database
 | 
			
		||||
        // 2. Validate the purchase
 | 
			
		||||
        // 3. Process the transaction
 | 
			
		||||
        // 4. Update the listing status
 | 
			
		||||
        // 5. Transfer the asset
 | 
			
		||||
        
 | 
			
		||||
        // For now, we'll just redirect to the marketplace
 | 
			
		||||
        Ok(HttpResponse::SeeOther()
 | 
			
		||||
            .insert_header((http::header::LOCATION, "/marketplace"))
 | 
			
		||||
            .finish())
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    // Cancel a listing
 | 
			
		||||
    pub async fn cancel_listing(
 | 
			
		||||
        tmpl: web::Data<Tera>,
 | 
			
		||||
        path: web::Path<String>,
 | 
			
		||||
    ) -> Result<HttpResponse> {
 | 
			
		||||
        let listing_id = path.into_inner();
 | 
			
		||||
        
 | 
			
		||||
        // In a real application, we would:
 | 
			
		||||
        // 1. Find the listing in the database
 | 
			
		||||
        // 2. Validate that the current user is the seller
 | 
			
		||||
        // 3. Update the listing status
 | 
			
		||||
        
 | 
			
		||||
        // For now, we'll just redirect to my listings
 | 
			
		||||
        Ok(HttpResponse::SeeOther()
 | 
			
		||||
            .insert_header((http::header::LOCATION, "/marketplace/my"))
 | 
			
		||||
            .finish())
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    // Generate mock listings for development
 | 
			
		||||
    pub fn get_mock_listings() -> Vec<Listing> {
 | 
			
		||||
        let assets = AssetController::get_mock_assets();
 | 
			
		||||
        let mut listings = Vec::new();
 | 
			
		||||
        
 | 
			
		||||
        // Mock user data
 | 
			
		||||
        let user_ids = vec!["user-123", "user-456", "user-789"];
 | 
			
		||||
        let user_names = vec!["John Doe", "Jane Smith", "Bob Johnson"];
 | 
			
		||||
        
 | 
			
		||||
        // Create some fixed price listings
 | 
			
		||||
        for i in 0..6 {
 | 
			
		||||
            let asset_index = i % assets.len();
 | 
			
		||||
            let asset = &assets[asset_index];
 | 
			
		||||
            let user_index = i % user_ids.len();
 | 
			
		||||
            
 | 
			
		||||
            let price = match asset.asset_type {
 | 
			
		||||
                AssetType::Token => 50.0 + (i as f64 * 10.0),
 | 
			
		||||
                AssetType::NFT => 500.0 + (i as f64 * 100.0),
 | 
			
		||||
                AssetType::RealEstate => 50000.0 + (i as f64 * 10000.0),
 | 
			
		||||
                AssetType::IntellectualProperty => 2000.0 + (i as f64 * 500.0),
 | 
			
		||||
                AssetType::PhysicalAsset => 1000.0 + (i as f64 * 200.0),
 | 
			
		||||
            };
 | 
			
		||||
            
 | 
			
		||||
            let mut listing = Listing::new(
 | 
			
		||||
                format!("{} for Sale", asset.name),
 | 
			
		||||
                format!("This is a great opportunity to own {}. {}", asset.name, asset.description),
 | 
			
		||||
                asset.id.clone(),
 | 
			
		||||
                asset.name.clone(),
 | 
			
		||||
                asset.asset_type.clone(),
 | 
			
		||||
                user_ids[user_index].to_string(),
 | 
			
		||||
                user_names[user_index].to_string(),
 | 
			
		||||
                price,
 | 
			
		||||
                "USD".to_string(),
 | 
			
		||||
                ListingType::FixedPrice,
 | 
			
		||||
                Some(Utc::now() + Duration::days(30)),
 | 
			
		||||
                vec!["digital".to_string(), "asset".to_string()],
 | 
			
		||||
                asset.image_url.clone(),
 | 
			
		||||
            );
 | 
			
		||||
            
 | 
			
		||||
            // Make some listings featured
 | 
			
		||||
            if i % 5 == 0 {
 | 
			
		||||
                listing.set_featured(true);
 | 
			
		||||
            }
 | 
			
		||||
            
 | 
			
		||||
            listings.push(listing);
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        // Create some auction listings
 | 
			
		||||
        for i in 0..4 {
 | 
			
		||||
            let asset_index = (i + 6) % assets.len();
 | 
			
		||||
            let asset = &assets[asset_index];
 | 
			
		||||
            let user_index = i % user_ids.len();
 | 
			
		||||
            
 | 
			
		||||
            let starting_price = match asset.asset_type {
 | 
			
		||||
                AssetType::Token => 40.0 + (i as f64 * 5.0),
 | 
			
		||||
                AssetType::NFT => 400.0 + (i as f64 * 50.0),
 | 
			
		||||
                AssetType::RealEstate => 40000.0 + (i as f64 * 5000.0),
 | 
			
		||||
                AssetType::IntellectualProperty => 1500.0 + (i as f64 * 300.0),
 | 
			
		||||
                AssetType::PhysicalAsset => 800.0 + (i as f64 * 100.0),
 | 
			
		||||
            };
 | 
			
		||||
            
 | 
			
		||||
            let mut listing = Listing::new(
 | 
			
		||||
                format!("Auction: {}", asset.name),
 | 
			
		||||
                format!("Bid on this amazing {}. {}", asset.name, asset.description),
 | 
			
		||||
                asset.id.clone(),
 | 
			
		||||
                asset.name.clone(),
 | 
			
		||||
                asset.asset_type.clone(),
 | 
			
		||||
                user_ids[user_index].to_string(),
 | 
			
		||||
                user_names[user_index].to_string(),
 | 
			
		||||
                starting_price,
 | 
			
		||||
                "USD".to_string(),
 | 
			
		||||
                ListingType::Auction,
 | 
			
		||||
                Some(Utc::now() + Duration::days(7)),
 | 
			
		||||
                vec!["auction".to_string(), "bidding".to_string()],
 | 
			
		||||
                asset.image_url.clone(),
 | 
			
		||||
            );
 | 
			
		||||
            
 | 
			
		||||
            // Add some bids to the auctions
 | 
			
		||||
            let num_bids = 2 + (i % 3);
 | 
			
		||||
            for j in 0..num_bids {
 | 
			
		||||
                let bidder_index = (j + 1) % user_ids.len();
 | 
			
		||||
                if bidder_index != user_index {  // Ensure seller isn't bidding
 | 
			
		||||
                    let bid_amount = starting_price * (1.0 + (0.1 * (j + 1) as f64));
 | 
			
		||||
                    let _ = listing.add_bid(
 | 
			
		||||
                        user_ids[bidder_index].to_string(),
 | 
			
		||||
                        user_names[bidder_index].to_string(),
 | 
			
		||||
                        bid_amount,
 | 
			
		||||
                        "USD".to_string(),
 | 
			
		||||
                    );
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            
 | 
			
		||||
            // Make some listings featured
 | 
			
		||||
            if i % 3 == 0 {
 | 
			
		||||
                listing.set_featured(true);
 | 
			
		||||
            }
 | 
			
		||||
            
 | 
			
		||||
            listings.push(listing);
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        // Create some exchange listings
 | 
			
		||||
        for i in 0..3 {
 | 
			
		||||
            let asset_index = (i + 10) % assets.len();
 | 
			
		||||
            let asset = &assets[asset_index];
 | 
			
		||||
            let user_index = i % user_ids.len();
 | 
			
		||||
            
 | 
			
		||||
            let value = match asset.asset_type {
 | 
			
		||||
                AssetType::Token => 60.0 + (i as f64 * 15.0),
 | 
			
		||||
                AssetType::NFT => 600.0 + (i as f64 * 150.0),
 | 
			
		||||
                AssetType::RealEstate => 60000.0 + (i as f64 * 15000.0),
 | 
			
		||||
                AssetType::IntellectualProperty => 2500.0 + (i as f64 * 600.0),
 | 
			
		||||
                AssetType::PhysicalAsset => 1200.0 + (i as f64 * 300.0),
 | 
			
		||||
            };
 | 
			
		||||
            
 | 
			
		||||
            let listing = Listing::new(
 | 
			
		||||
                format!("Trade: {}", asset.name),
 | 
			
		||||
                format!("Looking to exchange {} for another asset of similar value. Interested in NFTs and tokens.", asset.name),
 | 
			
		||||
                asset.id.clone(),
 | 
			
		||||
                asset.name.clone(),
 | 
			
		||||
                asset.asset_type.clone(),
 | 
			
		||||
                user_ids[user_index].to_string(),
 | 
			
		||||
                user_names[user_index].to_string(),
 | 
			
		||||
                value,  // Estimated value for exchange
 | 
			
		||||
                "USD".to_string(),
 | 
			
		||||
                ListingType::Exchange,
 | 
			
		||||
                Some(Utc::now() + Duration::days(60)),
 | 
			
		||||
                vec!["exchange".to_string(), "trade".to_string()],
 | 
			
		||||
                asset.image_url.clone(),
 | 
			
		||||
            );
 | 
			
		||||
            
 | 
			
		||||
            listings.push(listing);
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        // Create some sold listings
 | 
			
		||||
        for i in 0..5 {
 | 
			
		||||
            let asset_index = (i + 13) % assets.len();
 | 
			
		||||
            let asset = &assets[asset_index];
 | 
			
		||||
            let seller_index = i % user_ids.len();
 | 
			
		||||
            let buyer_index = (i + 1) % user_ids.len();
 | 
			
		||||
            
 | 
			
		||||
            let price = match asset.asset_type {
 | 
			
		||||
                AssetType::Token => 55.0 + (i as f64 * 12.0),
 | 
			
		||||
                AssetType::NFT => 550.0 + (i as f64 * 120.0),
 | 
			
		||||
                AssetType::RealEstate => 55000.0 + (i as f64 * 12000.0),
 | 
			
		||||
                AssetType::IntellectualProperty => 2200.0 + (i as f64 * 550.0),
 | 
			
		||||
                AssetType::PhysicalAsset => 1100.0 + (i as f64 * 220.0),
 | 
			
		||||
            };
 | 
			
		||||
            
 | 
			
		||||
            let sale_price = price * 0.95;  // Slight discount on sale
 | 
			
		||||
            
 | 
			
		||||
            let mut listing = Listing::new(
 | 
			
		||||
                format!("{} - SOLD", asset.name),
 | 
			
		||||
                format!("This {} was sold recently.", asset.name),
 | 
			
		||||
                asset.id.clone(),
 | 
			
		||||
                asset.name.clone(),
 | 
			
		||||
                asset.asset_type.clone(),
 | 
			
		||||
                user_ids[seller_index].to_string(),
 | 
			
		||||
                user_names[seller_index].to_string(),
 | 
			
		||||
                price,
 | 
			
		||||
                "USD".to_string(),
 | 
			
		||||
                ListingType::FixedPrice,
 | 
			
		||||
                None,
 | 
			
		||||
                vec!["sold".to_string()],
 | 
			
		||||
                asset.image_url.clone(),
 | 
			
		||||
            );
 | 
			
		||||
            
 | 
			
		||||
            // Mark as sold
 | 
			
		||||
            let _ = listing.mark_as_sold(
 | 
			
		||||
                user_ids[buyer_index].to_string(),
 | 
			
		||||
                user_names[buyer_index].to_string(),
 | 
			
		||||
                sale_price,
 | 
			
		||||
            );
 | 
			
		||||
            
 | 
			
		||||
            // Set sold date to be sometime in the past
 | 
			
		||||
            let days_ago = i as i64 + 1;
 | 
			
		||||
            listing.sold_at = Some(Utc::now() - Duration::days(days_ago));
 | 
			
		||||
            
 | 
			
		||||
            listings.push(listing);
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        // Create a few cancelled listings
 | 
			
		||||
        for i in 0..2 {
 | 
			
		||||
            let asset_index = (i + 18) % assets.len();
 | 
			
		||||
            let asset = &assets[asset_index];
 | 
			
		||||
            let user_index = i % user_ids.len();
 | 
			
		||||
            
 | 
			
		||||
            let price = match asset.asset_type {
 | 
			
		||||
                AssetType::Token => 45.0 + (i as f64 * 8.0),
 | 
			
		||||
                AssetType::NFT => 450.0 + (i as f64 * 80.0),
 | 
			
		||||
                AssetType::RealEstate => 45000.0 + (i as f64 * 8000.0),
 | 
			
		||||
                AssetType::IntellectualProperty => 1800.0 + (i as f64 * 400.0),
 | 
			
		||||
                AssetType::PhysicalAsset => 900.0 + (i as f64 * 180.0),
 | 
			
		||||
            };
 | 
			
		||||
            
 | 
			
		||||
            let mut listing = Listing::new(
 | 
			
		||||
                format!("{} - Cancelled", asset.name),
 | 
			
		||||
                format!("This listing for {} was cancelled.", asset.name),
 | 
			
		||||
                asset.id.clone(),
 | 
			
		||||
                asset.name.clone(),
 | 
			
		||||
                asset.asset_type.clone(),
 | 
			
		||||
                user_ids[user_index].to_string(),
 | 
			
		||||
                user_names[user_index].to_string(),
 | 
			
		||||
                price,
 | 
			
		||||
                "USD".to_string(),
 | 
			
		||||
                ListingType::FixedPrice,
 | 
			
		||||
                None,
 | 
			
		||||
                vec!["cancelled".to_string()],
 | 
			
		||||
                asset.image_url.clone(),
 | 
			
		||||
            );
 | 
			
		||||
            
 | 
			
		||||
            // Cancel the listing
 | 
			
		||||
            let _ = listing.cancel();
 | 
			
		||||
            
 | 
			
		||||
            listings.push(listing);
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        listings
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -7,5 +7,6 @@ pub mod governance;
 | 
			
		||||
pub mod flow;
 | 
			
		||||
pub mod contract;
 | 
			
		||||
pub mod asset;
 | 
			
		||||
pub mod marketplace;
 | 
			
		||||
 | 
			
		||||
// Re-export controllers for easier imports
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										295
									
								
								actix_mvc_app/src/models/marketplace.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										295
									
								
								actix_mvc_app/src/models/marketplace.rs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,295 @@
 | 
			
		||||
use chrono::{DateTime, Utc};
 | 
			
		||||
use serde::{Deserialize, Serialize};
 | 
			
		||||
use uuid::Uuid;
 | 
			
		||||
use crate::models::asset::{Asset, AssetType};
 | 
			
		||||
 | 
			
		||||
/// Status of a marketplace listing
 | 
			
		||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
 | 
			
		||||
pub enum ListingStatus {
 | 
			
		||||
    Active,
 | 
			
		||||
    Sold,
 | 
			
		||||
    Cancelled,
 | 
			
		||||
    Expired,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl ListingStatus {
 | 
			
		||||
    pub fn as_str(&self) -> &str {
 | 
			
		||||
        match self {
 | 
			
		||||
            ListingStatus::Active => "Active",
 | 
			
		||||
            ListingStatus::Sold => "Sold",
 | 
			
		||||
            ListingStatus::Cancelled => "Cancelled",
 | 
			
		||||
            ListingStatus::Expired => "Expired",
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Type of marketplace listing
 | 
			
		||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
 | 
			
		||||
pub enum ListingType {
 | 
			
		||||
    FixedPrice,
 | 
			
		||||
    Auction,
 | 
			
		||||
    Exchange,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl ListingType {
 | 
			
		||||
    pub fn as_str(&self) -> &str {
 | 
			
		||||
        match self {
 | 
			
		||||
            ListingType::FixedPrice => "Fixed Price",
 | 
			
		||||
            ListingType::Auction => "Auction",
 | 
			
		||||
            ListingType::Exchange => "Exchange",
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Represents a bid on an auction listing
 | 
			
		||||
#[derive(Debug, Clone, Serialize, Deserialize)]
 | 
			
		||||
pub struct Bid {
 | 
			
		||||
    pub id: String,
 | 
			
		||||
    pub listing_id: String,
 | 
			
		||||
    pub bidder_id: String,
 | 
			
		||||
    pub bidder_name: String,
 | 
			
		||||
    pub amount: f64,
 | 
			
		||||
    pub currency: String,
 | 
			
		||||
    pub created_at: DateTime<Utc>,
 | 
			
		||||
    pub status: BidStatus,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Status of a bid
 | 
			
		||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
 | 
			
		||||
pub enum BidStatus {
 | 
			
		||||
    Active,
 | 
			
		||||
    Accepted,
 | 
			
		||||
    Rejected,
 | 
			
		||||
    Cancelled,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl BidStatus {
 | 
			
		||||
    pub fn as_str(&self) -> &str {
 | 
			
		||||
        match self {
 | 
			
		||||
            BidStatus::Active => "Active",
 | 
			
		||||
            BidStatus::Accepted => "Accepted",
 | 
			
		||||
            BidStatus::Rejected => "Rejected",
 | 
			
		||||
            BidStatus::Cancelled => "Cancelled",
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Represents a marketplace listing
 | 
			
		||||
#[derive(Debug, Clone, Serialize, Deserialize)]
 | 
			
		||||
pub struct Listing {
 | 
			
		||||
    pub id: String,
 | 
			
		||||
    pub title: String,
 | 
			
		||||
    pub description: String,
 | 
			
		||||
    pub asset_id: String,
 | 
			
		||||
    pub asset_name: String,
 | 
			
		||||
    pub asset_type: AssetType,
 | 
			
		||||
    pub seller_id: String,
 | 
			
		||||
    pub seller_name: String,
 | 
			
		||||
    pub price: f64,
 | 
			
		||||
    pub currency: String,
 | 
			
		||||
    pub listing_type: ListingType,
 | 
			
		||||
    pub status: ListingStatus,
 | 
			
		||||
    pub created_at: DateTime<Utc>,
 | 
			
		||||
    pub updated_at: DateTime<Utc>,
 | 
			
		||||
    pub expires_at: Option<DateTime<Utc>>,
 | 
			
		||||
    pub sold_at: Option<DateTime<Utc>>,
 | 
			
		||||
    pub buyer_id: Option<String>,
 | 
			
		||||
    pub buyer_name: Option<String>,
 | 
			
		||||
    pub sale_price: Option<f64>,
 | 
			
		||||
    pub bids: Vec<Bid>,
 | 
			
		||||
    pub views: u32,
 | 
			
		||||
    pub featured: bool,
 | 
			
		||||
    pub tags: Vec<String>,
 | 
			
		||||
    pub image_url: Option<String>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl Listing {
 | 
			
		||||
    /// Creates a new listing
 | 
			
		||||
    pub fn new(
 | 
			
		||||
        title: String,
 | 
			
		||||
        description: String,
 | 
			
		||||
        asset_id: String,
 | 
			
		||||
        asset_name: String,
 | 
			
		||||
        asset_type: AssetType,
 | 
			
		||||
        seller_id: String,
 | 
			
		||||
        seller_name: String,
 | 
			
		||||
        price: f64,
 | 
			
		||||
        currency: String,
 | 
			
		||||
        listing_type: ListingType,
 | 
			
		||||
        expires_at: Option<DateTime<Utc>>,
 | 
			
		||||
        tags: Vec<String>,
 | 
			
		||||
        image_url: Option<String>,
 | 
			
		||||
    ) -> Self {
 | 
			
		||||
        let now = Utc::now();
 | 
			
		||||
        Self {
 | 
			
		||||
            id: format!("listing-{}", Uuid::new_v4().to_string()),
 | 
			
		||||
            title,
 | 
			
		||||
            description,
 | 
			
		||||
            asset_id,
 | 
			
		||||
            asset_name,
 | 
			
		||||
            asset_type,
 | 
			
		||||
            seller_id,
 | 
			
		||||
            seller_name,
 | 
			
		||||
            price,
 | 
			
		||||
            currency,
 | 
			
		||||
            listing_type,
 | 
			
		||||
            status: ListingStatus::Active,
 | 
			
		||||
            created_at: now,
 | 
			
		||||
            updated_at: now,
 | 
			
		||||
            expires_at,
 | 
			
		||||
            sold_at: None,
 | 
			
		||||
            buyer_id: None,
 | 
			
		||||
            buyer_name: None,
 | 
			
		||||
            sale_price: None,
 | 
			
		||||
            bids: Vec::new(),
 | 
			
		||||
            views: 0,
 | 
			
		||||
            featured: false,
 | 
			
		||||
            tags,
 | 
			
		||||
            image_url,
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Adds a bid to the listing
 | 
			
		||||
    pub fn add_bid(&mut self, bidder_id: String, bidder_name: String, amount: f64, currency: String) -> Result<(), String> {
 | 
			
		||||
        if self.status != ListingStatus::Active {
 | 
			
		||||
            return Err("Listing is not active".to_string());
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if self.listing_type != ListingType::Auction {
 | 
			
		||||
            return Err("Listing is not an auction".to_string());
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if currency != self.currency {
 | 
			
		||||
            return Err(format!("Currency mismatch: expected {}, got {}", self.currency, currency));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Check if bid amount is higher than current highest bid or starting price
 | 
			
		||||
        let highest_bid = self.highest_bid();
 | 
			
		||||
        let min_bid = match highest_bid {
 | 
			
		||||
            Some(bid) => bid.amount,
 | 
			
		||||
            None => self.price,
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        if amount <= min_bid {
 | 
			
		||||
            return Err(format!("Bid amount must be higher than {}", min_bid));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        let bid = Bid {
 | 
			
		||||
            id: format!("bid-{}", Uuid::new_v4().to_string()),
 | 
			
		||||
            listing_id: self.id.clone(),
 | 
			
		||||
            bidder_id,
 | 
			
		||||
            bidder_name,
 | 
			
		||||
            amount,
 | 
			
		||||
            currency,
 | 
			
		||||
            created_at: Utc::now(),
 | 
			
		||||
            status: BidStatus::Active,
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        self.bids.push(bid);
 | 
			
		||||
        self.updated_at = Utc::now();
 | 
			
		||||
 | 
			
		||||
        Ok(())
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Gets the highest bid on the listing
 | 
			
		||||
    pub fn highest_bid(&self) -> Option<&Bid> {
 | 
			
		||||
        self.bids.iter()
 | 
			
		||||
            .filter(|b| b.status == BidStatus::Active)
 | 
			
		||||
            .max_by(|a, b| a.amount.partial_cmp(&b.amount).unwrap())
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Marks the listing as sold
 | 
			
		||||
    pub fn mark_as_sold(&mut self, buyer_id: String, buyer_name: String, sale_price: f64) -> Result<(), String> {
 | 
			
		||||
        if self.status != ListingStatus::Active {
 | 
			
		||||
            return Err("Listing is not active".to_string());
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        self.status = ListingStatus::Sold;
 | 
			
		||||
        self.sold_at = Some(Utc::now());
 | 
			
		||||
        self.buyer_id = Some(buyer_id);
 | 
			
		||||
        self.buyer_name = Some(buyer_name);
 | 
			
		||||
        self.sale_price = Some(sale_price);
 | 
			
		||||
        self.updated_at = Utc::now();
 | 
			
		||||
 | 
			
		||||
        Ok(())
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Cancels the listing
 | 
			
		||||
    pub fn cancel(&mut self) -> Result<(), String> {
 | 
			
		||||
        if self.status != ListingStatus::Active {
 | 
			
		||||
            return Err("Listing is not active".to_string());
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        self.status = ListingStatus::Cancelled;
 | 
			
		||||
        self.updated_at = Utc::now();
 | 
			
		||||
 | 
			
		||||
        Ok(())
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Increments the view count
 | 
			
		||||
    pub fn increment_views(&mut self) {
 | 
			
		||||
        self.views += 1;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Sets the listing as featured
 | 
			
		||||
    pub fn set_featured(&mut self, featured: bool) {
 | 
			
		||||
        self.featured = featured;
 | 
			
		||||
        self.updated_at = Utc::now();
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Statistics for marketplace
 | 
			
		||||
#[derive(Debug, Clone, Serialize, Deserialize)]
 | 
			
		||||
pub struct MarketplaceStatistics {
 | 
			
		||||
    pub total_listings: usize,
 | 
			
		||||
    pub active_listings: usize,
 | 
			
		||||
    pub sold_listings: usize,
 | 
			
		||||
    pub total_value: f64,
 | 
			
		||||
    pub total_sales: f64,
 | 
			
		||||
    pub listings_by_type: std::collections::HashMap<String, usize>,
 | 
			
		||||
    pub sales_by_asset_type: std::collections::HashMap<String, f64>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl MarketplaceStatistics {
 | 
			
		||||
    pub fn new(listings: &[Listing]) -> Self {
 | 
			
		||||
        let mut total_value = 0.0;
 | 
			
		||||
        let mut total_sales = 0.0;
 | 
			
		||||
        let mut listings_by_type = std::collections::HashMap::new();
 | 
			
		||||
        let mut sales_by_asset_type = std::collections::HashMap::new();
 | 
			
		||||
 | 
			
		||||
        let active_listings = listings.iter()
 | 
			
		||||
            .filter(|l| l.status == ListingStatus::Active)
 | 
			
		||||
            .count();
 | 
			
		||||
 | 
			
		||||
        let sold_listings = listings.iter()
 | 
			
		||||
            .filter(|l| l.status == ListingStatus::Sold)
 | 
			
		||||
            .count();
 | 
			
		||||
 | 
			
		||||
        for listing in listings {
 | 
			
		||||
            if listing.status == ListingStatus::Active {
 | 
			
		||||
                total_value += listing.price;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if listing.status == ListingStatus::Sold {
 | 
			
		||||
                if let Some(sale_price) = listing.sale_price {
 | 
			
		||||
                    total_sales += sale_price;
 | 
			
		||||
                    let asset_type = listing.asset_type.as_str().to_string();
 | 
			
		||||
                    *sales_by_asset_type.entry(asset_type).or_insert(0.0) += sale_price;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            let listing_type = listing.listing_type.as_str().to_string();
 | 
			
		||||
            *listings_by_type.entry(listing_type).or_insert(0) += 1;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        Self {
 | 
			
		||||
            total_listings: listings.len(),
 | 
			
		||||
            active_listings,
 | 
			
		||||
            sold_listings,
 | 
			
		||||
            total_value,
 | 
			
		||||
            total_sales,
 | 
			
		||||
            listings_by_type,
 | 
			
		||||
            sales_by_asset_type,
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -6,8 +6,10 @@ pub mod governance;
 | 
			
		||||
pub mod flow;
 | 
			
		||||
pub mod contract;
 | 
			
		||||
pub mod asset;
 | 
			
		||||
pub mod marketplace;
 | 
			
		||||
 | 
			
		||||
// Re-export models for easier imports
 | 
			
		||||
pub use user::User;
 | 
			
		||||
pub use ticket::{Ticket, TicketComment, TicketStatus, TicketPriority};
 | 
			
		||||
pub use calendar::{CalendarEvent, CalendarViewMode};
 | 
			
		||||
pub use marketplace::{Listing, ListingStatus, ListingType, Bid, BidStatus, MarketplaceStatistics};
 | 
			
		||||
 
 | 
			
		||||
@@ -8,6 +8,7 @@ use crate::controllers::governance::GovernanceController;
 | 
			
		||||
use crate::controllers::flow::FlowController;
 | 
			
		||||
use crate::controllers::contract::ContractController;
 | 
			
		||||
use crate::controllers::asset::AssetController;
 | 
			
		||||
use crate::controllers::marketplace::MarketplaceController;
 | 
			
		||||
use crate::middleware::JwtAuth;
 | 
			
		||||
use crate::SESSION_KEY;
 | 
			
		||||
 | 
			
		||||
@@ -105,6 +106,20 @@ pub fn configure_routes(cfg: &mut web::ServiceConfig) {
 | 
			
		||||
                    .route("/{id}/transaction", web::post().to(AssetController::add_transaction))
 | 
			
		||||
                    .route("/{id}/status/{status}", web::post().to(AssetController::update_status))
 | 
			
		||||
            )
 | 
			
		||||
            
 | 
			
		||||
            // Marketplace routes
 | 
			
		||||
            .service(
 | 
			
		||||
                web::scope("/marketplace")
 | 
			
		||||
                    .route("", web::get().to(MarketplaceController::index))
 | 
			
		||||
                    .route("/listings", web::get().to(MarketplaceController::list_listings))
 | 
			
		||||
                    .route("/my", web::get().to(MarketplaceController::my_listings))
 | 
			
		||||
                    .route("/create", web::get().to(MarketplaceController::create_listing_form))
 | 
			
		||||
                    .route("/create", web::post().to(MarketplaceController::create_listing))
 | 
			
		||||
                    .route("/{id}", web::get().to(MarketplaceController::listing_detail))
 | 
			
		||||
                    .route("/{id}/bid", web::post().to(MarketplaceController::submit_bid))
 | 
			
		||||
                    .route("/{id}/purchase", web::post().to(MarketplaceController::purchase_listing))
 | 
			
		||||
                    .route("/{id}/cancel", web::post().to(MarketplaceController::cancel_listing))
 | 
			
		||||
            )
 | 
			
		||||
    );
 | 
			
		||||
    
 | 
			
		||||
    // Keep the /protected scope for any future routes that should be under that path
 | 
			
		||||
 
 | 
			
		||||
@@ -91,6 +91,7 @@
 | 
			
		||||
                            <li><a class="dropdown-item" href="/tickets/new">New Ticket</a></li>
 | 
			
		||||
                            <li><a class="dropdown-item" href="/my-tickets">My Tickets</a></li>
 | 
			
		||||
                            <li><a class="dropdown-item" href="/assets/my">My Assets</a></li>
 | 
			
		||||
                            <li><a class="dropdown-item" href="/marketplace/my">My Listings</a></li>
 | 
			
		||||
                            <li><a class="dropdown-item" href="/governance/my-votes">My Votes</a></li>
 | 
			
		||||
                            {% if user.role == "Admin" %}
 | 
			
		||||
                            <li><a class="dropdown-item" href="/admin">Admin Panel</a></li>
 | 
			
		||||
@@ -149,6 +150,11 @@
 | 
			
		||||
                        <i class="bi bi-coin me-2"></i> Digital Assets
 | 
			
		||||
                    </a>
 | 
			
		||||
                </li>
 | 
			
		||||
                <li class="nav-item">
 | 
			
		||||
                    <a class="nav-link d-flex align-items-center ps-3 py-2 {% if active_page == 'marketplace' %}active fw-bold border-start border-4 border-primary bg-light{% endif %}" href="/marketplace">
 | 
			
		||||
                        <i class="bi bi-shop me-2"></i> Marketplace
 | 
			
		||||
                    </a>
 | 
			
		||||
                </li>
 | 
			
		||||
                <!-- Markdown Editor link hidden
 | 
			
		||||
                <li class="nav-item">
 | 
			
		||||
                    <a class="nav-link d-flex align-items-center ps-3 py-2 {% if active_page == 'editor' %}active fw-bold border-start border-4 border-primary bg-light{% endif %}" href="/editor">
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										236
									
								
								actix_mvc_app/src/views/marketplace/create_listing.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										236
									
								
								actix_mvc_app/src/views/marketplace/create_listing.html
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,236 @@
 | 
			
		||||
{% extends "base.html" %}
 | 
			
		||||
 | 
			
		||||
{% block title %}Create Marketplace Listing{% endblock %}
 | 
			
		||||
 | 
			
		||||
{% block content %}
 | 
			
		||||
<div class="container-fluid px-4">
 | 
			
		||||
    <h1 class="mt-4">Create New Listing</h1>
 | 
			
		||||
    <ol class="breadcrumb mb-4">
 | 
			
		||||
        <li class="breadcrumb-item"><a href="/">Home</a></li>
 | 
			
		||||
        <li class="breadcrumb-item"><a href="/marketplace">Marketplace</a></li>
 | 
			
		||||
        <li class="breadcrumb-item active">Create Listing</li>
 | 
			
		||||
    </ol>
 | 
			
		||||
 | 
			
		||||
    <div class="row">
 | 
			
		||||
        <div class="col-lg-8">
 | 
			
		||||
            <div class="card mb-4">
 | 
			
		||||
                <div class="card-header">
 | 
			
		||||
                    <i class="bi bi-plus-circle"></i>
 | 
			
		||||
                    Listing Details
 | 
			
		||||
                </div>
 | 
			
		||||
                <div class="card-body">
 | 
			
		||||
                    {% if error %}
 | 
			
		||||
                        <div class="alert alert-danger">{{ error }}</div>
 | 
			
		||||
                    {% endif %}
 | 
			
		||||
 | 
			
		||||
                    <form action="/marketplace/create" method="post">
 | 
			
		||||
                        <div class="mb-3">
 | 
			
		||||
                            <label for="title" class="form-label">Listing Title</label>
 | 
			
		||||
                            <input type="text" class="form-control" id="title" name="title" required>
 | 
			
		||||
                            <div class="form-text">A clear, descriptive title for your listing.</div>
 | 
			
		||||
                        </div>
 | 
			
		||||
 | 
			
		||||
                        <div class="mb-3">
 | 
			
		||||
                            <label for="asset_id" class="form-label">Select Asset</label>
 | 
			
		||||
                            <select class="form-select" id="asset_id" name="asset_id" required>
 | 
			
		||||
                                <option value="" selected disabled>Choose an asset to list</option>
 | 
			
		||||
                                {% for asset in assets %}
 | 
			
		||||
                                    <option value="{{ asset.id }}" data-type="{{ asset.asset_type.as_str() }}" data-image="{{ asset.image_url }}">
 | 
			
		||||
                                        {{ asset.name }} ({{ asset.asset_type.as_str() }})
 | 
			
		||||
                                    </option>
 | 
			
		||||
                                {% endfor %}
 | 
			
		||||
                            </select>
 | 
			
		||||
                            <div class="form-text">Select one of your assets to list on the marketplace.</div>
 | 
			
		||||
                        </div>
 | 
			
		||||
 | 
			
		||||
                        <div class="mb-3">
 | 
			
		||||
                            <label for="description" class="form-label">Description</label>
 | 
			
		||||
                            <textarea class="form-control" id="description" name="description" rows="4" required></textarea>
 | 
			
		||||
                            <div class="form-text">Provide a detailed description of what you're selling.</div>
 | 
			
		||||
                        </div>
 | 
			
		||||
 | 
			
		||||
                        <div class="row mb-3">
 | 
			
		||||
                            <div class="col-md-6">
 | 
			
		||||
                                <label for="price" class="form-label">Price</label>
 | 
			
		||||
                                <div class="input-group">
 | 
			
		||||
                                    <span class="input-group-text">$</span>
 | 
			
		||||
                                    <input type="number" class="form-control" id="price" name="price" step="0.01" min="0.01" required>
 | 
			
		||||
                                </div>
 | 
			
		||||
                                <div class="form-text">Set a fair price for your asset.</div>
 | 
			
		||||
                            </div>
 | 
			
		||||
                            <div class="col-md-6">
 | 
			
		||||
                                <label for="currency" class="form-label">Currency</label>
 | 
			
		||||
                                <select class="form-select" id="currency" name="currency" required>
 | 
			
		||||
                                    <option value="USD" selected>USD</option>
 | 
			
		||||
                                    <option value="EUR">EUR</option>
 | 
			
		||||
                                    <option value="BTC">BTC</option>
 | 
			
		||||
                                    <option value="ETH">ETH</option>
 | 
			
		||||
                                    <option value="TFT">TFT</option>
 | 
			
		||||
                                </select>
 | 
			
		||||
                            </div>
 | 
			
		||||
                        </div>
 | 
			
		||||
 | 
			
		||||
                        <div class="row mb-3">
 | 
			
		||||
                            <div class="col-md-6">
 | 
			
		||||
                                <label for="listing_type" class="form-label">Listing Type</label>
 | 
			
		||||
                                <select class="form-select" id="listing_type" name="listing_type" required>
 | 
			
		||||
                                    {% for type in listing_types %}
 | 
			
		||||
                                        <option value="{{ type }}">{{ type }}</option>
 | 
			
		||||
                                    {% endfor %}
 | 
			
		||||
                                </select>
 | 
			
		||||
                                <div class="form-text">Choose how you want to sell your asset.</div>
 | 
			
		||||
                            </div>
 | 
			
		||||
                            <div class="col-md-6">
 | 
			
		||||
                                <label for="duration_days" class="form-label">Duration (Days)</label>
 | 
			
		||||
                                <input type="number" class="form-control" id="duration_days" name="duration_days" min="1" max="90" value="30">
 | 
			
		||||
                                <div class="form-text">How long should this listing be active? (1-90 days)</div>
 | 
			
		||||
                            </div>
 | 
			
		||||
                        </div>
 | 
			
		||||
 | 
			
		||||
                        <div class="mb-3">
 | 
			
		||||
                            <label for="tags" class="form-label">Tags</label>
 | 
			
		||||
                            <input type="text" class="form-control" id="tags" name="tags" placeholder="digital, rare, collectible">
 | 
			
		||||
                            <div class="form-text">Comma-separated tags to help buyers find your listing.</div>
 | 
			
		||||
                        </div>
 | 
			
		||||
 | 
			
		||||
                        <div class="mb-3 form-check">
 | 
			
		||||
                            <input type="checkbox" class="form-check-input" id="terms" required>
 | 
			
		||||
                            <label class="form-check-label" for="terms">
 | 
			
		||||
                                I agree to the <a href="#" target="_blank">marketplace terms and conditions</a>.
 | 
			
		||||
                            </label>
 | 
			
		||||
                        </div>
 | 
			
		||||
 | 
			
		||||
                        <div class="d-grid gap-2 d-md-flex justify-content-md-end">
 | 
			
		||||
                            <a href="/marketplace" class="btn btn-secondary me-md-2">Cancel</a>
 | 
			
		||||
                            <button type="submit" class="btn btn-primary">Create Listing</button>
 | 
			
		||||
                        </div>
 | 
			
		||||
                    </form>
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
 | 
			
		||||
        <div class="col-lg-4">
 | 
			
		||||
            <!-- Asset Preview -->
 | 
			
		||||
            <div class="card mb-4">
 | 
			
		||||
                <div class="card-header">
 | 
			
		||||
                    <i class="bi bi-eye"></i>
 | 
			
		||||
                    Asset Preview
 | 
			
		||||
                </div>
 | 
			
		||||
                <div class="card-body text-center">
 | 
			
		||||
                    <div id="asset-preview-container">
 | 
			
		||||
                        <div class="bg-light d-flex align-items-center justify-content-center rounded mb-3" style="height: 200px;">
 | 
			
		||||
                            <i class="bi bi-image text-secondary" style="font-size: 3rem;"></i>
 | 
			
		||||
                        </div>
 | 
			
		||||
                        <p class="text-muted">Select an asset to preview</p>
 | 
			
		||||
                    </div>
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
 | 
			
		||||
            <!-- Listing Tips -->
 | 
			
		||||
            <div class="card mb-4">
 | 
			
		||||
                <div class="card-header">
 | 
			
		||||
                    <i class="bi bi-lightbulb"></i>
 | 
			
		||||
                    Listing Tips
 | 
			
		||||
                </div>
 | 
			
		||||
                <div class="card-body">
 | 
			
		||||
                    <ul class="list-unstyled">
 | 
			
		||||
                        <li class="mb-2">
 | 
			
		||||
                            <i class="bi bi-check-circle text-success me-2"></i>
 | 
			
		||||
                            Use a clear, descriptive title
 | 
			
		||||
                        </li>
 | 
			
		||||
                        <li class="mb-2">
 | 
			
		||||
                            <i class="bi bi-check-circle text-success me-2"></i>
 | 
			
		||||
                            Include detailed information about your asset
 | 
			
		||||
                        </li>
 | 
			
		||||
                        <li class="mb-2">
 | 
			
		||||
                            <i class="bi bi-check-circle text-success me-2"></i>
 | 
			
		||||
                            Set a competitive price
 | 
			
		||||
                        </li>
 | 
			
		||||
                        <li class="mb-2">
 | 
			
		||||
                            <i class="bi bi-check-circle text-success me-2"></i>
 | 
			
		||||
                            Add relevant tags to improve discoverability
 | 
			
		||||
                        </li>
 | 
			
		||||
                        <li class="mb-2">
 | 
			
		||||
                            <i class="bi bi-check-circle text-success me-2"></i>
 | 
			
		||||
                            Choose the right listing type for your asset
 | 
			
		||||
                        </li>
 | 
			
		||||
                    </ul>
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
</div>
 | 
			
		||||
{% endblock %}
 | 
			
		||||
 | 
			
		||||
{% block scripts %}
 | 
			
		||||
<script>
 | 
			
		||||
    document.addEventListener('DOMContentLoaded', function() {
 | 
			
		||||
        const assetSelect = document.getElementById('asset_id');
 | 
			
		||||
        const previewContainer = document.getElementById('asset-preview-container');
 | 
			
		||||
        const listingTypeSelect = document.getElementById('listing_type');
 | 
			
		||||
        
 | 
			
		||||
        // Update preview when asset is selected
 | 
			
		||||
        assetSelect.addEventListener('change', function() {
 | 
			
		||||
            const selectedOption = assetSelect.options[assetSelect.selectedIndex];
 | 
			
		||||
            const assetType = selectedOption.getAttribute('data-type');
 | 
			
		||||
            const imageUrl = selectedOption.getAttribute('data-image');
 | 
			
		||||
            const assetName = selectedOption.text;
 | 
			
		||||
            
 | 
			
		||||
            let previewHtml = '';
 | 
			
		||||
            
 | 
			
		||||
            if (imageUrl) {
 | 
			
		||||
                previewHtml = `
 | 
			
		||||
                    <img src="${imageUrl}" class="img-fluid rounded mb-3" alt="${assetName}" style="max-height: 200px;">
 | 
			
		||||
                `;
 | 
			
		||||
            } else {
 | 
			
		||||
                previewHtml = `
 | 
			
		||||
                    <div class="bg-light d-flex align-items-center justify-content-center rounded mb-3" style="height: 200px;">
 | 
			
		||||
                        <i class="bi bi-collection text-secondary" style="font-size: 3rem;"></i>
 | 
			
		||||
                    </div>
 | 
			
		||||
                `;
 | 
			
		||||
            }
 | 
			
		||||
            
 | 
			
		||||
            previewHtml += `
 | 
			
		||||
                <h5>${assetName}</h5>
 | 
			
		||||
                <span class="badge bg-primary mb-2">${assetType}</span>
 | 
			
		||||
                <p class="text-muted">This is how your asset will appear to buyers.</p>
 | 
			
		||||
            `;
 | 
			
		||||
            
 | 
			
		||||
            previewContainer.innerHTML = previewHtml;
 | 
			
		||||
            
 | 
			
		||||
            // Suggest listing type based on asset type
 | 
			
		||||
            if (assetType === 'NFT') {
 | 
			
		||||
                listingTypeSelect.value = 'Auction';
 | 
			
		||||
            } else if (assetType === 'Token') {
 | 
			
		||||
                listingTypeSelect.value = 'Fixed Price';
 | 
			
		||||
            } else if (assetType === 'RealEstate') {
 | 
			
		||||
                listingTypeSelect.value = 'Fixed Price';
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
        
 | 
			
		||||
        // Show/hide duration field based on listing type
 | 
			
		||||
        listingTypeSelect.addEventListener('change', function() {
 | 
			
		||||
            const durationField = document.getElementById('duration_days');
 | 
			
		||||
            const durationFieldParent = durationField.parentElement;
 | 
			
		||||
            
 | 
			
		||||
            if (listingTypeSelect.value === 'Auction') {
 | 
			
		||||
                durationFieldParent.style.display = 'block';
 | 
			
		||||
                durationField.required = true;
 | 
			
		||||
                if (!durationField.value) {
 | 
			
		||||
                    durationField.value = 7; // Default auction duration
 | 
			
		||||
                }
 | 
			
		||||
            } else if (listingTypeSelect.value === 'Exchange') {
 | 
			
		||||
                durationFieldParent.style.display = 'block';
 | 
			
		||||
                durationField.required = true;
 | 
			
		||||
                if (!durationField.value) {
 | 
			
		||||
                    durationField.value = 30; // Default exchange duration
 | 
			
		||||
                }
 | 
			
		||||
            } else {
 | 
			
		||||
                // For fixed price, duration is optional
 | 
			
		||||
                durationFieldParent.style.display = 'block';
 | 
			
		||||
                durationField.required = false;
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
    });
 | 
			
		||||
</script>
 | 
			
		||||
{% endblock %}
 | 
			
		||||
							
								
								
									
										293
									
								
								actix_mvc_app/src/views/marketplace/index.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										293
									
								
								actix_mvc_app/src/views/marketplace/index.html
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,293 @@
 | 
			
		||||
{% extends "base.html" %}
 | 
			
		||||
 | 
			
		||||
{% block title %}Digital Assets Marketplace{% endblock %}
 | 
			
		||||
 | 
			
		||||
{% block content %}
 | 
			
		||||
<div class="container-fluid px-4">
 | 
			
		||||
    <h1 class="mt-4">Digital Assets Marketplace</h1>
 | 
			
		||||
    <ol class="breadcrumb mb-4">
 | 
			
		||||
        <li class="breadcrumb-item"><a href="/">Home</a></li>
 | 
			
		||||
        <li class="breadcrumb-item active">Marketplace</li>
 | 
			
		||||
    </ol>
 | 
			
		||||
 | 
			
		||||
    <!-- Stats Cards -->
 | 
			
		||||
    <div class="row">
 | 
			
		||||
        <div class="col-xl-3 col-md-6">
 | 
			
		||||
            <div class="card bg-primary text-white mb-4">
 | 
			
		||||
                <div class="card-body">
 | 
			
		||||
                    <h2 class="display-4">{{ stats.active_listings }}</h2>
 | 
			
		||||
                    <p class="mb-0">Active Listings</p>
 | 
			
		||||
                </div>
 | 
			
		||||
                <div class="card-footer d-flex align-items-center justify-content-between">
 | 
			
		||||
                    <a class="small text-white stretched-link" href="/marketplace/listings">View Details</a>
 | 
			
		||||
                    <div class="small text-white"><i class="bi bi-arrow-right"></i></div>
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div class="col-xl-3 col-md-6">
 | 
			
		||||
            <div class="card bg-success text-white mb-4">
 | 
			
		||||
                <div class="card-body">
 | 
			
		||||
                    <h2 class="display-4">${{ stats.total_value }}</h2>
 | 
			
		||||
                    <p class="mb-0">Total Market Value</p>
 | 
			
		||||
                </div>
 | 
			
		||||
                <div class="card-footer d-flex align-items-center justify-content-between">
 | 
			
		||||
                    <a class="small text-white stretched-link" href="/marketplace/listings">View Details</a>
 | 
			
		||||
                    <div class="small text-white"><i class="bi bi-arrow-right"></i></div>
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div class="col-xl-3 col-md-6">
 | 
			
		||||
            <div class="card bg-warning text-white mb-4">
 | 
			
		||||
                <div class="card-body">
 | 
			
		||||
                    <h2 class="display-4">{{ stats.total_listings }}</h2>
 | 
			
		||||
                    <p class="mb-0">Total Listings</p>
 | 
			
		||||
                </div>
 | 
			
		||||
                <div class="card-footer d-flex align-items-center justify-content-between">
 | 
			
		||||
                    <a class="small text-white stretched-link" href="/marketplace/listings">View Details</a>
 | 
			
		||||
                    <div class="small text-white"><i class="bi bi-arrow-right"></i></div>
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div class="col-xl-3 col-md-6">
 | 
			
		||||
            <div class="card bg-info text-white mb-4">
 | 
			
		||||
                <div class="card-body">
 | 
			
		||||
                    <h2 class="display-4">${{ stats.total_sales }}</h2>
 | 
			
		||||
                    <p class="mb-0">Total Sales</p>
 | 
			
		||||
                </div>
 | 
			
		||||
                <div class="card-footer d-flex align-items-center justify-content-between">
 | 
			
		||||
                    <a class="small text-white stretched-link" href="/marketplace/listings">View Details</a>
 | 
			
		||||
                    <div class="small text-white"><i class="bi bi-arrow-right"></i></div>
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    <!-- Quick Actions -->
 | 
			
		||||
    <div class="row mb-4">
 | 
			
		||||
        <div class="col-12">
 | 
			
		||||
            <div class="card">
 | 
			
		||||
                <div class="card-header">
 | 
			
		||||
                    <i class="bi bi-lightning-charge"></i>
 | 
			
		||||
                    Quick Actions
 | 
			
		||||
                </div>
 | 
			
		||||
                <div class="card-body">
 | 
			
		||||
                    <div class="d-flex justify-content-around">
 | 
			
		||||
                        <a href="/marketplace/create" class="btn btn-primary">
 | 
			
		||||
                            <i class="bi bi-plus-circle"></i> List New Asset
 | 
			
		||||
                        </a>
 | 
			
		||||
                        <a href="/marketplace/listings" class="btn btn-success">
 | 
			
		||||
                            <i class="bi bi-search"></i> Browse Listings
 | 
			
		||||
                        </a>
 | 
			
		||||
                        <a href="/marketplace/my" class="btn btn-info">
 | 
			
		||||
                            <i class="bi bi-person"></i> My Listings
 | 
			
		||||
                        </a>
 | 
			
		||||
                        <a href="/assets/my" class="btn btn-secondary">
 | 
			
		||||
                            <i class="bi bi-collection"></i> My Assets
 | 
			
		||||
                        </a>
 | 
			
		||||
                    </div>
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    <!-- Featured Listings -->
 | 
			
		||||
    <div class="row mb-4">
 | 
			
		||||
        <div class="col-12">
 | 
			
		||||
            <div class="card">
 | 
			
		||||
                <div class="card-header">
 | 
			
		||||
                    <i class="bi bi-star"></i>
 | 
			
		||||
                    Featured Listings
 | 
			
		||||
                </div>
 | 
			
		||||
                <div class="card-body">
 | 
			
		||||
                    <div class="row">
 | 
			
		||||
                        {% if featured_listings|length > 0 %}
 | 
			
		||||
                            {% for listing in featured_listings %}
 | 
			
		||||
                                <div class="col-md-3 mb-3">
 | 
			
		||||
                                    <div class="card h-100">
 | 
			
		||||
                                        <div class="badge bg-warning text-dark position-absolute" style="top: 0.5rem; right: 0.5rem">Featured</div>
 | 
			
		||||
                                        {% if listing.image_url %}
 | 
			
		||||
                                            <img src="{{ listing.image_url }}" class="card-img-top" alt="{{ listing.title }}" style="height: 180px; object-fit: cover;">
 | 
			
		||||
                                        {% else %}
 | 
			
		||||
                                            <div class="card-img-top bg-light d-flex align-items-center justify-content-center" style="height: 180px;">
 | 
			
		||||
                                                <i class="bi bi-image text-secondary" style="font-size: 3rem;"></i>
 | 
			
		||||
                                            </div>
 | 
			
		||||
                                        {% endif %}
 | 
			
		||||
                                        <div class="card-body">
 | 
			
		||||
                                            <h5 class="card-title">{{ listing.title }}</h5>
 | 
			
		||||
                                            <p class="card-text text-truncate">{{ listing.description }}</p>
 | 
			
		||||
                                            <div class="d-flex justify-content-between align-items-center">
 | 
			
		||||
                                                <span class="badge bg-primary">{{ listing.listing_type.as_str() }}</span>
 | 
			
		||||
                                                <span class="badge bg-secondary">{{ listing.asset_type.as_str() }}</span>
 | 
			
		||||
                                            </div>
 | 
			
		||||
                                        </div>
 | 
			
		||||
                                        <div class="card-footer">
 | 
			
		||||
                                            <div class="d-flex justify-content-between align-items-center">
 | 
			
		||||
                                                <strong>${{ listing.price }}</strong>
 | 
			
		||||
                                                <a href="/marketplace/{{ listing.id }}" class="btn btn-sm btn-outline-primary">View</a>
 | 
			
		||||
                                            </div>
 | 
			
		||||
                                        </div>
 | 
			
		||||
                                    </div>
 | 
			
		||||
                                </div>
 | 
			
		||||
                            {% endfor %}
 | 
			
		||||
                        {% else %}
 | 
			
		||||
                            <div class="col-12">
 | 
			
		||||
                                <p class="text-center">No featured listings available at this time.</p>
 | 
			
		||||
                            </div>
 | 
			
		||||
                        {% endif %}
 | 
			
		||||
                    </div>
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    <!-- Recent Listings and Sales -->
 | 
			
		||||
    <div class="row">
 | 
			
		||||
        <!-- Recent Listings -->
 | 
			
		||||
        <div class="col-lg-8">
 | 
			
		||||
            <div class="card mb-4">
 | 
			
		||||
                <div class="card-header">
 | 
			
		||||
                    <i class="bi bi-clock"></i>
 | 
			
		||||
                    Recent Listings
 | 
			
		||||
                </div>
 | 
			
		||||
                <div class="card-body">
 | 
			
		||||
                    <div class="table-responsive">
 | 
			
		||||
                        <table class="table table-bordered table-hover">
 | 
			
		||||
                            <thead>
 | 
			
		||||
                                <tr>
 | 
			
		||||
                                    <th>Asset</th>
 | 
			
		||||
                                    <th>Type</th>
 | 
			
		||||
                                    <th>Price</th>
 | 
			
		||||
                                    <th>Listing Type</th>
 | 
			
		||||
                                    <th>Seller</th>
 | 
			
		||||
                                    <th>Listed</th>
 | 
			
		||||
                                    <th>Action</th>
 | 
			
		||||
                                </tr>
 | 
			
		||||
                            </thead>
 | 
			
		||||
                            <tbody>
 | 
			
		||||
                                {% if recent_listings|length > 0 %}
 | 
			
		||||
                                    {% for listing in recent_listings %}
 | 
			
		||||
                                        <tr>
 | 
			
		||||
                                            <td>
 | 
			
		||||
                                                <div class="d-flex align-items-center">
 | 
			
		||||
                                                    {% if listing.image_url %}
 | 
			
		||||
                                                        <img src="{{ listing.image_url }}" alt="{{ listing.asset_name }}" class="me-2" style="width: 30px; height: 30px; object-fit: cover;">
 | 
			
		||||
                                                    {% else %}
 | 
			
		||||
                                                        <i class="bi bi-collection me-2"></i>
 | 
			
		||||
                                                    {% endif %}
 | 
			
		||||
                                                    {{ listing.asset_name }}
 | 
			
		||||
                                                </div>
 | 
			
		||||
                                            </td>
 | 
			
		||||
                                            <td>
 | 
			
		||||
                                                {% if listing.asset_type.as_str() == "Token" %}
 | 
			
		||||
                                                    <span class="badge bg-primary">{{ listing.asset_type.as_str() }}</span>
 | 
			
		||||
                                                {% elif listing.asset_type.as_str() == "NFT" %}
 | 
			
		||||
                                                    <span class="badge bg-info">{{ listing.asset_type.as_str() }}</span>
 | 
			
		||||
                                                {% elif listing.asset_type.as_str() == "RealEstate" %}
 | 
			
		||||
                                                    <span class="badge bg-success">Real Estate</span>
 | 
			
		||||
                                                {% elif listing.asset_type.as_str() == "IntellectualProperty" %}
 | 
			
		||||
                                                    <span class="badge bg-warning text-dark">IP</span>
 | 
			
		||||
                                                {% else %}
 | 
			
		||||
                                                    <span class="badge bg-secondary">{{ listing.asset_type.as_str() }}</span>
 | 
			
		||||
                                                {% endif %}
 | 
			
		||||
                                            </td>
 | 
			
		||||
                                            <td>${{ listing.price }}</td>
 | 
			
		||||
                                            <td>{{ listing.listing_type.as_str() }}</td>
 | 
			
		||||
                                            <td>{{ listing.seller_name }}</td>
 | 
			
		||||
                                            <td>{{ listing.created_at|date }}</td>
 | 
			
		||||
                                            <td>
 | 
			
		||||
                                                <a href="/marketplace/{{ listing.id }}" class="btn btn-sm btn-outline-primary">View</a>
 | 
			
		||||
                                            </td>
 | 
			
		||||
                                        </tr>
 | 
			
		||||
                                    {% endfor %}
 | 
			
		||||
                                {% else %}
 | 
			
		||||
                                    <tr>
 | 
			
		||||
                                        <td colspan="7" class="text-center">No recent listings available.</td>
 | 
			
		||||
                                    </tr>
 | 
			
		||||
                                {% endif %}
 | 
			
		||||
                            </tbody>
 | 
			
		||||
                        </table>
 | 
			
		||||
                    </div>
 | 
			
		||||
                </div>
 | 
			
		||||
                <div class="card-footer">
 | 
			
		||||
                    <a href="/marketplace/listings" class="btn btn-sm btn-primary">View All Listings</a>
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
 | 
			
		||||
        <!-- Recent Sales -->
 | 
			
		||||
        <div class="col-lg-4">
 | 
			
		||||
            <div class="card mb-4">
 | 
			
		||||
                <div class="card-header">
 | 
			
		||||
                    <i class="bi bi-bag-check"></i>
 | 
			
		||||
                    Recent Sales
 | 
			
		||||
                </div>
 | 
			
		||||
                <div class="card-body">
 | 
			
		||||
                    <div class="table-responsive">
 | 
			
		||||
                        <table class="table table-bordered table-hover">
 | 
			
		||||
                            <thead>
 | 
			
		||||
                                <tr>
 | 
			
		||||
                                    <th>Asset</th>
 | 
			
		||||
                                    <th>Price</th>
 | 
			
		||||
                                    <th>Date</th>
 | 
			
		||||
                                </tr>
 | 
			
		||||
                            </thead>
 | 
			
		||||
                            <tbody>
 | 
			
		||||
                                {% if recent_sales|length > 0 %}
 | 
			
		||||
                                    {% for listing in recent_sales %}
 | 
			
		||||
                                        <tr>
 | 
			
		||||
                                            <td>
 | 
			
		||||
                                                <div class="d-flex align-items-center">
 | 
			
		||||
                                                    {% if listing.image_url %}
 | 
			
		||||
                                                        <img src="{{ listing.image_url }}" alt="{{ listing.asset_name }}" class="me-2" style="width: 30px; height: 30px; object-fit: cover;">
 | 
			
		||||
                                                    {% else %}
 | 
			
		||||
                                                        <i class="bi bi-collection me-2"></i>
 | 
			
		||||
                                                    {% endif %}
 | 
			
		||||
                                                    {{ listing.asset_name }}
 | 
			
		||||
                                                </div>
 | 
			
		||||
                                            </td>
 | 
			
		||||
                                            <td>${{ listing.sale_price }}</td>
 | 
			
		||||
                                            <td>{{ listing.sold_at|date }}</td>
 | 
			
		||||
                                        </tr>
 | 
			
		||||
                                    {% endfor %}
 | 
			
		||||
                                {% else %}
 | 
			
		||||
                                    <tr>
 | 
			
		||||
                                        <td colspan="3" class="text-center">No recent sales available.</td>
 | 
			
		||||
                                    </tr>
 | 
			
		||||
                                {% endif %}
 | 
			
		||||
                            </tbody>
 | 
			
		||||
                        </table>
 | 
			
		||||
                    </div>
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
 | 
			
		||||
            <!-- Listing Types Distribution -->
 | 
			
		||||
            <div class="card mb-4">
 | 
			
		||||
                <div class="card-header">
 | 
			
		||||
                    <i class="bi bi-pie-chart"></i>
 | 
			
		||||
                    Listing Types
 | 
			
		||||
                </div>
 | 
			
		||||
                <div class="card-body">
 | 
			
		||||
                    <div class="table-responsive">
 | 
			
		||||
                        <table class="table table-bordered">
 | 
			
		||||
                            <thead>
 | 
			
		||||
                                <tr>
 | 
			
		||||
                                    <th>Type</th>
 | 
			
		||||
                                    <th>Count</th>
 | 
			
		||||
                                </tr>
 | 
			
		||||
                            </thead>
 | 
			
		||||
                            <tbody>
 | 
			
		||||
                                {% for type, count in stats.listings_by_type %}
 | 
			
		||||
                                    <tr>
 | 
			
		||||
                                        <td>{{ type }}</td>
 | 
			
		||||
                                        <td>{{ count }}</td>
 | 
			
		||||
                                    </tr>
 | 
			
		||||
                                {% endfor %}
 | 
			
		||||
                            </tbody>
 | 
			
		||||
                        </table>
 | 
			
		||||
                    </div>
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
</div>
 | 
			
		||||
{% endblock %}
 | 
			
		||||
							
								
								
									
										350
									
								
								actix_mvc_app/src/views/marketplace/listing_detail.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										350
									
								
								actix_mvc_app/src/views/marketplace/listing_detail.html
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,350 @@
 | 
			
		||||
{% extends "base.html" %}
 | 
			
		||||
 | 
			
		||||
{% block title %}{{ listing.title }} | Marketplace{% endblock %}
 | 
			
		||||
 | 
			
		||||
{% block content %}
 | 
			
		||||
<div class="container-fluid px-4">
 | 
			
		||||
    <h1 class="mt-4">Listing Details</h1>
 | 
			
		||||
    <ol class="breadcrumb mb-4">
 | 
			
		||||
        <li class="breadcrumb-item"><a href="/">Home</a></li>
 | 
			
		||||
        <li class="breadcrumb-item"><a href="/marketplace">Marketplace</a></li>
 | 
			
		||||
        <li class="breadcrumb-item"><a href="/marketplace/listings">Listings</a></li>
 | 
			
		||||
        <li class="breadcrumb-item active">{{ listing.title }}</li>
 | 
			
		||||
    </ol>
 | 
			
		||||
 | 
			
		||||
    <!-- Listing Details -->
 | 
			
		||||
    <div class="row">
 | 
			
		||||
        <!-- Left Column: Image and Actions -->
 | 
			
		||||
        <div class="col-md-5">
 | 
			
		||||
            <div class="card mb-4">
 | 
			
		||||
                <div class="card-body text-center">
 | 
			
		||||
                    {% if listing.image_url %}
 | 
			
		||||
                        <img src="{{ listing.image_url }}" alt="{{ listing.title }}" class="img-fluid rounded mb-3" style="max-height: 350px; object-fit: contain;">
 | 
			
		||||
                    {% else %}
 | 
			
		||||
                        <div class="bg-light d-flex align-items-center justify-content-center rounded mb-3" style="height: 350px;">
 | 
			
		||||
                            <i class="bi bi-image text-secondary" style="font-size: 5rem;"></i>
 | 
			
		||||
                        </div>
 | 
			
		||||
                    {% endif %}
 | 
			
		||||
 | 
			
		||||
                    <div class="d-grid gap-2">
 | 
			
		||||
                        {% if listing.listing_type.as_str() == "Fixed Price" %}
 | 
			
		||||
                            <button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#purchaseModal">
 | 
			
		||||
                                <i class="bi bi-cart"></i> Purchase Now
 | 
			
		||||
                            </button>
 | 
			
		||||
                        {% elif listing.listing_type.as_str() == "Auction" %}
 | 
			
		||||
                            <button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#bidModal">
 | 
			
		||||
                                <i class="bi bi-hammer"></i> Place Bid
 | 
			
		||||
                            </button>
 | 
			
		||||
                        {% elif listing.listing_type.as_str() == "Exchange" %}
 | 
			
		||||
                            <button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#offerModal">
 | 
			
		||||
                                <i class="bi bi-arrow-left-right"></i> Make Exchange Offer
 | 
			
		||||
                            </button>
 | 
			
		||||
                        {% endif %}
 | 
			
		||||
 | 
			
		||||
                        {% if listing.seller_id == user_id %}
 | 
			
		||||
                            <button type="button" class="btn btn-danger" data-bs-toggle="modal" data-bs-target="#cancelModal">
 | 
			
		||||
                                <i class="bi bi-x-circle"></i> Cancel Listing
 | 
			
		||||
                            </button>
 | 
			
		||||
                        {% endif %}
 | 
			
		||||
                    </div>
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
 | 
			
		||||
            <!-- Asset Information -->
 | 
			
		||||
            <div class="card mb-4">
 | 
			
		||||
                <div class="card-header">
 | 
			
		||||
                    <i class="bi bi-info-circle"></i>
 | 
			
		||||
                    Asset Information
 | 
			
		||||
                </div>
 | 
			
		||||
                <div class="card-body">
 | 
			
		||||
                    <p><strong>Asset Name:</strong> {{ listing.asset_name }}</p>
 | 
			
		||||
                    <p><strong>Asset Type:</strong> 
 | 
			
		||||
                        {% if listing.asset_type.as_str() == "Token" %}
 | 
			
		||||
                            <span class="badge bg-primary">{{ listing.asset_type.as_str() }}</span>
 | 
			
		||||
                        {% elif listing.asset_type.as_str() == "NFT" %}
 | 
			
		||||
                            <span class="badge bg-info">{{ listing.asset_type.as_str() }}</span>
 | 
			
		||||
                        {% elif listing.asset_type.as_str() == "RealEstate" %}
 | 
			
		||||
                            <span class="badge bg-success">Real Estate</span>
 | 
			
		||||
                        {% elif listing.asset_type.as_str() == "IntellectualProperty" %}
 | 
			
		||||
                            <span class="badge bg-warning text-dark">Intellectual Property</span>
 | 
			
		||||
                        {% else %}
 | 
			
		||||
                            <span class="badge bg-secondary">{{ listing.asset_type.as_str() }}</span>
 | 
			
		||||
                        {% endif %}
 | 
			
		||||
                    </p>
 | 
			
		||||
                    <p><strong>Asset ID:</strong> <code>{{ listing.asset_id }}</code></p>
 | 
			
		||||
                    <a href="/assets/{{ listing.asset_id }}" class="btn btn-sm btn-outline-secondary">
 | 
			
		||||
                        <i class="bi bi-eye"></i> View Asset Details
 | 
			
		||||
                    </a>
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
 | 
			
		||||
        <!-- Right Column: Details and Bids -->
 | 
			
		||||
        <div class="col-md-7">
 | 
			
		||||
            <div class="card mb-4">
 | 
			
		||||
                <div class="card-header">
 | 
			
		||||
                    <div class="d-flex justify-content-between align-items-center">
 | 
			
		||||
                        <div>
 | 
			
		||||
                            <i class="bi bi-tag"></i>
 | 
			
		||||
                            Listing Details
 | 
			
		||||
                        </div>
 | 
			
		||||
                        <div>
 | 
			
		||||
                            <span class="badge bg-{{ listing.status.as_str() == 'Active' ? 'success' : 'secondary' }}">
 | 
			
		||||
                                {{ listing.status.as_str() }}
 | 
			
		||||
                            </span>
 | 
			
		||||
                        </div>
 | 
			
		||||
                    </div>
 | 
			
		||||
                </div>
 | 
			
		||||
                <div class="card-body">
 | 
			
		||||
                    <h2 class="card-title mb-3">{{ listing.title }}</h2>
 | 
			
		||||
                    
 | 
			
		||||
                    <div class="d-flex justify-content-between align-items-center mb-3">
 | 
			
		||||
                        <div>
 | 
			
		||||
                            <span class="badge bg-primary">{{ listing.listing_type.as_str() }}</span>
 | 
			
		||||
                            {% if listing.featured %}
 | 
			
		||||
                                <span class="badge bg-warning text-dark">Featured</span>
 | 
			
		||||
                            {% endif %}
 | 
			
		||||
                        </div>
 | 
			
		||||
                        <div>
 | 
			
		||||
                            <h3 class="text-primary mb-0">${{ listing.price }}</h3>
 | 
			
		||||
                        </div>
 | 
			
		||||
                    </div>
 | 
			
		||||
 | 
			
		||||
                    <p class="card-text">{{ listing.description }}</p>
 | 
			
		||||
 | 
			
		||||
                    <hr>
 | 
			
		||||
 | 
			
		||||
                    <div class="row mb-3">
 | 
			
		||||
                        <div class="col-md-6">
 | 
			
		||||
                            <p><strong>Seller:</strong> {{ listing.seller_name }}</p>
 | 
			
		||||
                            <p><strong>Listed:</strong> {{ listing.created_at|date }}</p>
 | 
			
		||||
                        </div>
 | 
			
		||||
                        <div class="col-md-6">
 | 
			
		||||
                            <p><strong>Currency:</strong> {{ listing.currency }}</p>
 | 
			
		||||
                            <p><strong>Expires:</strong> 
 | 
			
		||||
                                {% if listing.expires_at %}
 | 
			
		||||
                                    {{ listing.expires_at|date }}
 | 
			
		||||
                                {% else %}
 | 
			
		||||
                                    <span class="text-muted">No expiration</span>
 | 
			
		||||
                                {% endif %}
 | 
			
		||||
                            </p>
 | 
			
		||||
                        </div>
 | 
			
		||||
                    </div>
 | 
			
		||||
 | 
			
		||||
                    {% if listing.tags|length > 0 %}
 | 
			
		||||
                        <div class="mb-3">
 | 
			
		||||
                            <strong>Tags:</strong>
 | 
			
		||||
                            {% for tag in listing.tags %}
 | 
			
		||||
                                <span class="badge bg-secondary me-1">{{ tag }}</span>
 | 
			
		||||
                            {% endfor %}
 | 
			
		||||
                        </div>
 | 
			
		||||
                    {% endif %}
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
 | 
			
		||||
            <!-- Bids Section (for Auctions) -->
 | 
			
		||||
            {% if listing.listing_type.as_str() == "Auction" %}
 | 
			
		||||
                <div class="card mb-4">
 | 
			
		||||
                    <div class="card-header">
 | 
			
		||||
                        <i class="bi bi-list-ol"></i>
 | 
			
		||||
                        Bids
 | 
			
		||||
                    </div>
 | 
			
		||||
                    <div class="card-body">
 | 
			
		||||
                        {% if listing.bids|length > 0 %}
 | 
			
		||||
                            <div class="table-responsive">
 | 
			
		||||
                                <table class="table table-bordered table-hover">
 | 
			
		||||
                                    <thead>
 | 
			
		||||
                                        <tr>
 | 
			
		||||
                                            <th>Bidder</th>
 | 
			
		||||
                                            <th>Amount</th>
 | 
			
		||||
                                            <th>Time</th>
 | 
			
		||||
                                            <th>Status</th>
 | 
			
		||||
                                        </tr>
 | 
			
		||||
                                    </thead>
 | 
			
		||||
                                    <tbody>
 | 
			
		||||
                                        {% for bid in listing.bids %}
 | 
			
		||||
                                            <tr>
 | 
			
		||||
                                                <td>{{ bid.bidder_name }}</td>
 | 
			
		||||
                                                <td>${{ bid.amount }}</td>
 | 
			
		||||
                                                <td>{{ bid.created_at|date }}</td>
 | 
			
		||||
                                                <td>
 | 
			
		||||
                                                    <span class="badge bg-{{ bid.status.as_str() == 'Active' ? 'success' : 'secondary' }}">
 | 
			
		||||
                                                        {{ bid.status.as_str() }}
 | 
			
		||||
                                                    </span>
 | 
			
		||||
                                                </td>
 | 
			
		||||
                                            </tr>
 | 
			
		||||
                                        {% endfor %}
 | 
			
		||||
                                    </tbody>
 | 
			
		||||
                                </table>
 | 
			
		||||
                            </div>
 | 
			
		||||
                            <p class="mt-2"><strong>Current Highest Bid:</strong> ${{ listing.highest_bid().amount }}</p>
 | 
			
		||||
                        {% else %}
 | 
			
		||||
                            <p>No bids yet. Be the first to bid!</p>
 | 
			
		||||
                            <p><strong>Starting Price:</strong> ${{ listing.price }}</p>
 | 
			
		||||
                        {% endif %}
 | 
			
		||||
                    </div>
 | 
			
		||||
                </div>
 | 
			
		||||
            {% endif %}
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    <!-- Similar Listings -->
 | 
			
		||||
    <div class="row mb-4">
 | 
			
		||||
        <div class="col-12">
 | 
			
		||||
            <div class="card">
 | 
			
		||||
                <div class="card-header">
 | 
			
		||||
                    <i class="bi bi-grid"></i>
 | 
			
		||||
                    Similar Listings
 | 
			
		||||
                </div>
 | 
			
		||||
                <div class="card-body">
 | 
			
		||||
                    <div class="row">
 | 
			
		||||
                        {% if similar_listings|length > 0 %}
 | 
			
		||||
                            {% for similar in similar_listings %}
 | 
			
		||||
                                <div class="col-md-3 mb-3">
 | 
			
		||||
                                    <div class="card h-100">
 | 
			
		||||
                                        {% if similar.image_url %}
 | 
			
		||||
                                            <img src="{{ similar.image_url }}" class="card-img-top" alt="{{ similar.title }}" style="height: 150px; object-fit: cover;">
 | 
			
		||||
                                        {% else %}
 | 
			
		||||
                                            <div class="card-img-top bg-light d-flex align-items-center justify-content-center" style="height: 150px;">
 | 
			
		||||
                                                <i class="bi bi-image text-secondary" style="font-size: 2rem;"></i>
 | 
			
		||||
                                            </div>
 | 
			
		||||
                                        {% endif %}
 | 
			
		||||
                                        <div class="card-body">
 | 
			
		||||
                                            <h5 class="card-title">{{ similar.title }}</h5>
 | 
			
		||||
                                            <div class="d-flex justify-content-between align-items-center">
 | 
			
		||||
                                                <span class="badge bg-primary">{{ similar.listing_type.as_str() }}</span>
 | 
			
		||||
                                                <span class="badge bg-secondary">{{ similar.asset_type.as_str() }}</span>
 | 
			
		||||
                                            </div>
 | 
			
		||||
                                        </div>
 | 
			
		||||
                                        <div class="card-footer">
 | 
			
		||||
                                            <div class="d-flex justify-content-between align-items-center">
 | 
			
		||||
                                                <strong>${{ similar.price }}</strong>
 | 
			
		||||
                                                <a href="/marketplace/{{ similar.id }}" class="btn btn-sm btn-outline-primary">View</a>
 | 
			
		||||
                                            </div>
 | 
			
		||||
                                        </div>
 | 
			
		||||
                                    </div>
 | 
			
		||||
                                </div>
 | 
			
		||||
                            {% endfor %}
 | 
			
		||||
                        {% else %}
 | 
			
		||||
                            <div class="col-12">
 | 
			
		||||
                                <p class="text-center">No similar listings found.</p>
 | 
			
		||||
                            </div>
 | 
			
		||||
                        {% endif %}
 | 
			
		||||
                    </div>
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
<!-- Purchase Modal -->
 | 
			
		||||
<div class="modal fade" id="purchaseModal" tabindex="-1" aria-labelledby="purchaseModalLabel" aria-hidden="true">
 | 
			
		||||
    <div class="modal-dialog">
 | 
			
		||||
        <div class="modal-content">
 | 
			
		||||
            <div class="modal-header">
 | 
			
		||||
                <h5 class="modal-title" id="purchaseModalLabel">Purchase Asset</h5>
 | 
			
		||||
                <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
 | 
			
		||||
            </div>
 | 
			
		||||
            <form action="/marketplace/{{ listing.id }}/purchase" method="post">
 | 
			
		||||
                <div class="modal-body">
 | 
			
		||||
                    <p>You are about to purchase <strong>{{ listing.asset_name }}</strong> for <strong>${{ listing.price }}</strong>.</p>
 | 
			
		||||
                    
 | 
			
		||||
                    <div class="alert alert-info">
 | 
			
		||||
                        <h6>Purchase Details:</h6>
 | 
			
		||||
                        <ul>
 | 
			
		||||
                            <li>Asset: {{ listing.asset_name }}</li>
 | 
			
		||||
                            <li>Price: ${{ listing.price }} {{ listing.currency }}</li>
 | 
			
		||||
                            <li>Seller: {{ listing.seller_name }}</li>
 | 
			
		||||
                        </ul>
 | 
			
		||||
                    </div>
 | 
			
		||||
                    
 | 
			
		||||
                    <div class="mb-3">
 | 
			
		||||
                        <div class="form-check">
 | 
			
		||||
                            <input class="form-check-input" type="checkbox" id="agree-terms" name="agree_to_terms" required>
 | 
			
		||||
                            <label class="form-check-label" for="agree-terms">
 | 
			
		||||
                                I agree to the <a href="#" target="_blank">terms and conditions</a> of this purchase.
 | 
			
		||||
                            </label>
 | 
			
		||||
                        </div>
 | 
			
		||||
                    </div>
 | 
			
		||||
                </div>
 | 
			
		||||
                <div class="modal-footer">
 | 
			
		||||
                    <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
 | 
			
		||||
                    <button type="submit" class="btn btn-primary">Confirm Purchase</button>
 | 
			
		||||
                </div>
 | 
			
		||||
            </form>
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
<!-- Bid Modal -->
 | 
			
		||||
<div class="modal fade" id="bidModal" tabindex="-1" aria-labelledby="bidModalLabel" aria-hidden="true">
 | 
			
		||||
    <div class="modal-dialog">
 | 
			
		||||
        <div class="modal-content">
 | 
			
		||||
            <div class="modal-header">
 | 
			
		||||
                <h5 class="modal-title" id="bidModalLabel">Place Bid</h5>
 | 
			
		||||
                <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
 | 
			
		||||
            </div>
 | 
			
		||||
            <form action="/marketplace/{{ listing.id }}/bid" method="post">
 | 
			
		||||
                <div class="modal-body">
 | 
			
		||||
                    <p>You are placing a bid on <strong>{{ listing.asset_name }}</strong>.</p>
 | 
			
		||||
                    
 | 
			
		||||
                    <div class="alert alert-info">
 | 
			
		||||
                        <h6>Auction Details:</h6>
 | 
			
		||||
                        <ul>
 | 
			
		||||
                            <li>Asset: {{ listing.asset_name }}</li>
 | 
			
		||||
                            <li>Starting Price: ${{ listing.price }} {{ listing.currency }}</li>
 | 
			
		||||
                            {% if listing.highest_bid() %}
 | 
			
		||||
                                <li>Current Highest Bid: ${{ listing.highest_bid().amount }} {{ listing.currency }}</li>
 | 
			
		||||
                                <li>Minimum Bid: ${{ listing.highest_bid().amount + 1 }} {{ listing.currency }}</li>
 | 
			
		||||
                            {% else %}
 | 
			
		||||
                                <li>Minimum Bid: ${{ listing.price + 1 }} {{ listing.currency }}</li>
 | 
			
		||||
                            {% endif %}
 | 
			
		||||
                        </ul>
 | 
			
		||||
                    </div>
 | 
			
		||||
                    
 | 
			
		||||
                    <div class="mb-3">
 | 
			
		||||
                        <label for="bid-amount" class="form-label">Your Bid Amount ({{ listing.currency }})</label>
 | 
			
		||||
                        <div class="input-group">
 | 
			
		||||
                            <span class="input-group-text">$</span>
 | 
			
		||||
                            <input type="number" class="form-control" id="bid-amount" name="amount" step="0.01" min="{{ listing.highest_bid() ? listing.highest_bid().amount + 1 : listing.price + 1 }}" required>
 | 
			
		||||
                        </div>
 | 
			
		||||
                    </div>
 | 
			
		||||
                    
 | 
			
		||||
                    <input type="hidden" name="currency" value="{{ listing.currency }}">
 | 
			
		||||
                </div>
 | 
			
		||||
                <div class="modal-footer">
 | 
			
		||||
                    <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
 | 
			
		||||
                    <button type="submit" class="btn btn-primary">Place Bid</button>
 | 
			
		||||
                </div>
 | 
			
		||||
            </form>
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
<!-- Cancel Modal -->
 | 
			
		||||
<div class="modal fade" id="cancelModal" tabindex="-1" aria-labelledby="cancelModalLabel" aria-hidden="true">
 | 
			
		||||
    <div class="modal-dialog">
 | 
			
		||||
        <div class="modal-content">
 | 
			
		||||
            <div class="modal-header">
 | 
			
		||||
                <h5 class="modal-title" id="cancelModalLabel">Cancel Listing</h5>
 | 
			
		||||
                <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
 | 
			
		||||
            </div>
 | 
			
		||||
            <form action="/marketplace/{{ listing.id }}/cancel" method="post">
 | 
			
		||||
                <div class="modal-body">
 | 
			
		||||
                    <p>Are you sure you want to cancel this listing for <strong>{{ listing.asset_name }}</strong>?</p>
 | 
			
		||||
                    <div class="alert alert-warning">
 | 
			
		||||
                        <p>This action cannot be undone. The listing will be marked as cancelled and removed from the marketplace.</p>
 | 
			
		||||
                        {% if listing.bids|length > 0 %}
 | 
			
		||||
                            <p><strong>Note:</strong> This listing has active bids. Cancelling will notify all bidders.</p>
 | 
			
		||||
                        {% endif %}
 | 
			
		||||
                    </div>
 | 
			
		||||
                </div>
 | 
			
		||||
                <div class="modal-footer">
 | 
			
		||||
                    <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">No, Keep Listing</button>
 | 
			
		||||
                    <button type="submit" class="btn btn-danger">Yes, Cancel Listing</button>
 | 
			
		||||
                </div>
 | 
			
		||||
            </form>
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
</div>
 | 
			
		||||
{% endblock %}
 | 
			
		||||
							
								
								
									
										294
									
								
								actix_mvc_app/src/views/marketplace/listings.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										294
									
								
								actix_mvc_app/src/views/marketplace/listings.html
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,294 @@
 | 
			
		||||
{% extends "base.html" %}
 | 
			
		||||
 | 
			
		||||
{% block title %}Marketplace Listings{% endblock %}
 | 
			
		||||
 | 
			
		||||
{% block content %}
 | 
			
		||||
<div class="container-fluid px-4">
 | 
			
		||||
    <h1 class="mt-4">Marketplace Listings</h1>
 | 
			
		||||
    <ol class="breadcrumb mb-4">
 | 
			
		||||
        <li class="breadcrumb-item"><a href="/">Home</a></li>
 | 
			
		||||
        <li class="breadcrumb-item"><a href="/marketplace">Marketplace</a></li>
 | 
			
		||||
        <li class="breadcrumb-item active">Listings</li>
 | 
			
		||||
    </ol>
 | 
			
		||||
 | 
			
		||||
    <!-- Filters -->
 | 
			
		||||
    <div class="card mb-4">
 | 
			
		||||
        <div class="card-header">
 | 
			
		||||
            <i class="bi bi-funnel"></i>
 | 
			
		||||
            Filter Listings
 | 
			
		||||
        </div>
 | 
			
		||||
        <div class="card-body">
 | 
			
		||||
            <form id="filter-form" class="row g-3">
 | 
			
		||||
                <div class="col-md-3">
 | 
			
		||||
                    <label for="asset-type" class="form-label">Asset Type</label>
 | 
			
		||||
                    <select id="asset-type" class="form-select">
 | 
			
		||||
                        <option value="">All Types</option>
 | 
			
		||||
                        {% for type in asset_types %}
 | 
			
		||||
                            <option value="{{ type }}">{{ type }}</option>
 | 
			
		||||
                        {% endfor %}
 | 
			
		||||
                    </select>
 | 
			
		||||
                </div>
 | 
			
		||||
                <div class="col-md-3">
 | 
			
		||||
                    <label for="listing-type" class="form-label">Listing Type</label>
 | 
			
		||||
                    <select id="listing-type" class="form-select">
 | 
			
		||||
                        <option value="">All Listings</option>
 | 
			
		||||
                        {% for type in listing_types %}
 | 
			
		||||
                            <option value="{{ type }}">{{ type }}</option>
 | 
			
		||||
                        {% endfor %}
 | 
			
		||||
                    </select>
 | 
			
		||||
                </div>
 | 
			
		||||
                <div class="col-md-3">
 | 
			
		||||
                    <label for="price-min" class="form-label">Min Price</label>
 | 
			
		||||
                    <input type="number" class="form-control" id="price-min" placeholder="Min $">
 | 
			
		||||
                </div>
 | 
			
		||||
                <div class="col-md-3">
 | 
			
		||||
                    <label for="price-max" class="form-label">Max Price</label>
 | 
			
		||||
                    <input type="number" class="form-control" id="price-max" placeholder="Max $">
 | 
			
		||||
                </div>
 | 
			
		||||
                <div class="col-12">
 | 
			
		||||
                    <label for="search" class="form-label">Search</label>
 | 
			
		||||
                    <input type="text" class="form-control" id="search" placeholder="Search by name, description, or tags">
 | 
			
		||||
                </div>
 | 
			
		||||
                <div class="col-12">
 | 
			
		||||
                    <button type="submit" class="btn btn-primary">Apply Filters</button>
 | 
			
		||||
                    <button type="reset" class="btn btn-secondary">Reset</button>
 | 
			
		||||
                </div>
 | 
			
		||||
            </form>
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    <!-- View Toggle -->
 | 
			
		||||
    <div class="mb-3">
 | 
			
		||||
        <div class="btn-group" role="group" aria-label="View Toggle">
 | 
			
		||||
            <button type="button" class="btn btn-outline-primary active" id="grid-view-btn">
 | 
			
		||||
                <i class="bi bi-grid"></i> Grid View
 | 
			
		||||
            </button>
 | 
			
		||||
            <button type="button" class="btn btn-outline-primary" id="list-view-btn">
 | 
			
		||||
                <i class="bi bi-list"></i> List View
 | 
			
		||||
            </button>
 | 
			
		||||
        </div>
 | 
			
		||||
        <a href="/marketplace/create" class="btn btn-success float-end">
 | 
			
		||||
            <i class="bi bi-plus-circle"></i> List New Asset
 | 
			
		||||
        </a>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    <!-- Grid View -->
 | 
			
		||||
    <div id="grid-view">
 | 
			
		||||
        <div class="row">
 | 
			
		||||
            {% if listings|length > 0 %}
 | 
			
		||||
                {% for listing in listings %}
 | 
			
		||||
                    <div class="col-xl-3 col-lg-4 col-md-6 mb-4 listing-item" 
 | 
			
		||||
                         data-asset-type="{{ listing.asset_type.as_str() }}"
 | 
			
		||||
                         data-listing-type="{{ listing.listing_type.as_str() }}"
 | 
			
		||||
                         data-price="{{ listing.price }}">
 | 
			
		||||
                        <div class="card h-100">
 | 
			
		||||
                            {% if listing.featured %}
 | 
			
		||||
                                <div class="badge bg-warning text-dark position-absolute" style="top: 0.5rem; right: 0.5rem">Featured</div>
 | 
			
		||||
                            {% endif %}
 | 
			
		||||
                            {% if listing.image_url %}
 | 
			
		||||
                                <img src="{{ listing.image_url }}" class="card-img-top" alt="{{ listing.title }}" style="height: 180px; object-fit: cover;">
 | 
			
		||||
                            {% else %}
 | 
			
		||||
                                <div class="card-img-top bg-light d-flex align-items-center justify-content-center" style="height: 180px;">
 | 
			
		||||
                                    <i class="bi bi-image text-secondary" style="font-size: 3rem;"></i>
 | 
			
		||||
                                </div>
 | 
			
		||||
                            {% endif %}
 | 
			
		||||
                            <div class="card-body">
 | 
			
		||||
                                <h5 class="card-title">{{ listing.title }}</h5>
 | 
			
		||||
                                <p class="card-text text-truncate">{{ listing.description }}</p>
 | 
			
		||||
                                <div class="d-flex justify-content-between align-items-center mb-2">
 | 
			
		||||
                                    <span class="badge bg-primary">{{ listing.listing_type.as_str() }}</span>
 | 
			
		||||
                                    {% if listing.asset_type.as_str() == "Token" %}
 | 
			
		||||
                                        <span class="badge bg-primary">{{ listing.asset_type.as_str() }}</span>
 | 
			
		||||
                                    {% elif listing.asset_type.as_str() == "NFT" %}
 | 
			
		||||
                                        <span class="badge bg-info">{{ listing.asset_type.as_str() }}</span>
 | 
			
		||||
                                    {% elif listing.asset_type.as_str() == "RealEstate" %}
 | 
			
		||||
                                        <span class="badge bg-success">Real Estate</span>
 | 
			
		||||
                                    {% elif listing.asset_type.as_str() == "IntellectualProperty" %}
 | 
			
		||||
                                        <span class="badge bg-warning text-dark">IP</span>
 | 
			
		||||
                                    {% else %}
 | 
			
		||||
                                        <span class="badge bg-secondary">{{ listing.asset_type.as_str() }}</span>
 | 
			
		||||
                                    {% endif %}
 | 
			
		||||
                                </div>
 | 
			
		||||
                                <div class="d-flex justify-content-between align-items-center">
 | 
			
		||||
                                    <small class="text-muted">Listed by {{ listing.seller_name }}</small>
 | 
			
		||||
                                </div>
 | 
			
		||||
                            </div>
 | 
			
		||||
                            <div class="card-footer">
 | 
			
		||||
                                <div class="d-flex justify-content-between align-items-center">
 | 
			
		||||
                                    <strong>${{ listing.price }}</strong>
 | 
			
		||||
                                    <a href="/marketplace/{{ listing.id }}" class="btn btn-sm btn-outline-primary">View</a>
 | 
			
		||||
                                </div>
 | 
			
		||||
                            </div>
 | 
			
		||||
                        </div>
 | 
			
		||||
                    </div>
 | 
			
		||||
                {% endfor %}
 | 
			
		||||
            {% else %}
 | 
			
		||||
                <div class="col-12">
 | 
			
		||||
                    <div class="alert alert-info">
 | 
			
		||||
                        No listings found. <a href="/marketplace/create">Create a new listing</a>
 | 
			
		||||
                    </div>
 | 
			
		||||
                </div>
 | 
			
		||||
            {% endif %}
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    <!-- List View -->
 | 
			
		||||
    <div id="list-view" style="display: none;">
 | 
			
		||||
        <div class="card mb-4">
 | 
			
		||||
            <div class="card-header">
 | 
			
		||||
                <i class="bi bi-list-ul"></i>
 | 
			
		||||
                All Listings
 | 
			
		||||
            </div>
 | 
			
		||||
            <div class="card-body">
 | 
			
		||||
                <div class="table-responsive">
 | 
			
		||||
                    <table class="table table-bordered table-hover">
 | 
			
		||||
                        <thead>
 | 
			
		||||
                            <tr>
 | 
			
		||||
                                <th>Asset</th>
 | 
			
		||||
                                <th>Title</th>
 | 
			
		||||
                                <th>Type</th>
 | 
			
		||||
                                <th>Price</th>
 | 
			
		||||
                                <th>Listing Type</th>
 | 
			
		||||
                                <th>Seller</th>
 | 
			
		||||
                                <th>Listed</th>
 | 
			
		||||
                                <th>Expires</th>
 | 
			
		||||
                                <th>Action</th>
 | 
			
		||||
                            </tr>
 | 
			
		||||
                        </thead>
 | 
			
		||||
                        <tbody>
 | 
			
		||||
                            {% if listings|length > 0 %}
 | 
			
		||||
                                {% for listing in listings %}
 | 
			
		||||
                                    <tr class="listing-item" 
 | 
			
		||||
                                        data-asset-type="{{ listing.asset_type.as_str() }}"
 | 
			
		||||
                                        data-listing-type="{{ listing.listing_type.as_str() }}"
 | 
			
		||||
                                        data-price="{{ listing.price }}">
 | 
			
		||||
                                        <td>
 | 
			
		||||
                                            <div class="d-flex align-items-center">
 | 
			
		||||
                                                {% if listing.image_url %}
 | 
			
		||||
                                                    <img src="{{ listing.image_url }}" alt="{{ listing.asset_name }}" class="me-2" style="width: 30px; height: 30px; object-fit: cover;">
 | 
			
		||||
                                                {% else %}
 | 
			
		||||
                                                    <i class="bi bi-collection me-2"></i>
 | 
			
		||||
                                                {% endif %}
 | 
			
		||||
                                            </div>
 | 
			
		||||
                                        </td>
 | 
			
		||||
                                        <td>{{ listing.title }}</td>
 | 
			
		||||
                                        <td>
 | 
			
		||||
                                            {% if listing.asset_type.as_str() == "Token" %}
 | 
			
		||||
                                                <span class="badge bg-primary">{{ listing.asset_type.as_str() }}</span>
 | 
			
		||||
                                            {% elif listing.asset_type.as_str() == "NFT" %}
 | 
			
		||||
                                                <span class="badge bg-info">{{ listing.asset_type.as_str() }}</span>
 | 
			
		||||
                                            {% elif listing.asset_type.as_str() == "RealEstate" %}
 | 
			
		||||
                                                <span class="badge bg-success">Real Estate</span>
 | 
			
		||||
                                            {% elif listing.asset_type.as_str() == "IntellectualProperty" %}
 | 
			
		||||
                                                <span class="badge bg-warning text-dark">IP</span>
 | 
			
		||||
                                            {% else %}
 | 
			
		||||
                                                <span class="badge bg-secondary">{{ listing.asset_type.as_str() }}</span>
 | 
			
		||||
                                            {% endif %}
 | 
			
		||||
                                        </td>
 | 
			
		||||
                                        <td>${{ listing.price }}</td>
 | 
			
		||||
                                        <td>{{ listing.listing_type.as_str() }}</td>
 | 
			
		||||
                                        <td>{{ listing.seller_name }}</td>
 | 
			
		||||
                                        <td>{{ listing.created_at|date }}</td>
 | 
			
		||||
                                        <td>
 | 
			
		||||
                                            {% if listing.expires_at %}
 | 
			
		||||
                                                {{ listing.expires_at|date }}
 | 
			
		||||
                                            {% else %}
 | 
			
		||||
                                                <span class="text-muted">N/A</span>
 | 
			
		||||
                                            {% endif %}
 | 
			
		||||
                                        </td>
 | 
			
		||||
                                        <td>
 | 
			
		||||
                                            <a href="/marketplace/{{ listing.id }}" class="btn btn-sm btn-outline-primary">View</a>
 | 
			
		||||
                                        </td>
 | 
			
		||||
                                    </tr>
 | 
			
		||||
                                {% endfor %}
 | 
			
		||||
                            {% else %}
 | 
			
		||||
                                <tr>
 | 
			
		||||
                                    <td colspan="9" class="text-center">No listings available.</td>
 | 
			
		||||
                                </tr>
 | 
			
		||||
                            {% endif %}
 | 
			
		||||
                        </tbody>
 | 
			
		||||
                    </table>
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
{% endblock %}
 | 
			
		||||
 | 
			
		||||
{% block scripts %}
 | 
			
		||||
<script>
 | 
			
		||||
    document.addEventListener('DOMContentLoaded', function() {
 | 
			
		||||
        // View toggle
 | 
			
		||||
        const gridViewBtn = document.getElementById('grid-view-btn');
 | 
			
		||||
        const listViewBtn = document.getElementById('list-view-btn');
 | 
			
		||||
        const gridView = document.getElementById('grid-view');
 | 
			
		||||
        const listView = document.getElementById('list-view');
 | 
			
		||||
 | 
			
		||||
        gridViewBtn.addEventListener('click', function() {
 | 
			
		||||
            gridView.style.display = 'block';
 | 
			
		||||
            listView.style.display = 'none';
 | 
			
		||||
            gridViewBtn.classList.add('active');
 | 
			
		||||
            listViewBtn.classList.remove('active');
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        listViewBtn.addEventListener('click', function() {
 | 
			
		||||
            gridView.style.display = 'none';
 | 
			
		||||
            listView.style.display = 'block';
 | 
			
		||||
            listViewBtn.classList.add('active');
 | 
			
		||||
            gridViewBtn.classList.remove('active');
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        // Filtering
 | 
			
		||||
        const filterForm = document.getElementById('filter-form');
 | 
			
		||||
        const assetTypeSelect = document.getElementById('asset-type');
 | 
			
		||||
        const listingTypeSelect = document.getElementById('listing-type');
 | 
			
		||||
        const priceMinInput = document.getElementById('price-min');
 | 
			
		||||
        const priceMaxInput = document.getElementById('price-max');
 | 
			
		||||
        const searchInput = document.getElementById('search');
 | 
			
		||||
        const listingItems = document.querySelectorAll('.listing-item');
 | 
			
		||||
 | 
			
		||||
        filterForm.addEventListener('submit', function(e) {
 | 
			
		||||
            e.preventDefault();
 | 
			
		||||
            applyFilters();
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        filterForm.addEventListener('reset', function() {
 | 
			
		||||
            setTimeout(function() {
 | 
			
		||||
                applyFilters();
 | 
			
		||||
            }, 10);
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        function applyFilters() {
 | 
			
		||||
            const assetType = assetTypeSelect.value;
 | 
			
		||||
            const listingType = listingTypeSelect.value;
 | 
			
		||||
            const priceMin = priceMinInput.value ? parseFloat(priceMinInput.value) : 0;
 | 
			
		||||
            const priceMax = priceMaxInput.value ? parseFloat(priceMaxInput.value) : Infinity;
 | 
			
		||||
            const searchTerm = searchInput.value.toLowerCase();
 | 
			
		||||
 | 
			
		||||
            listingItems.forEach(function(item) {
 | 
			
		||||
                const itemAssetType = item.getAttribute('data-asset-type');
 | 
			
		||||
                const itemListingType = item.getAttribute('data-listing-type');
 | 
			
		||||
                const itemPrice = parseFloat(item.getAttribute('data-price'));
 | 
			
		||||
                const itemTitle = item.querySelector('.card-title') ? 
 | 
			
		||||
                                  item.querySelector('.card-title').textContent.toLowerCase() : '';
 | 
			
		||||
                const itemDescription = item.querySelector('.card-text') ? 
 | 
			
		||||
                                       item.querySelector('.card-text').textContent.toLowerCase() : '';
 | 
			
		||||
 | 
			
		||||
                const assetTypeMatch = !assetType || itemAssetType === assetType;
 | 
			
		||||
                const listingTypeMatch = !listingType || itemListingType === listingType;
 | 
			
		||||
                const priceMatch = itemPrice >= priceMin && itemPrice <= priceMax;
 | 
			
		||||
                const searchMatch = !searchTerm || 
 | 
			
		||||
                                   itemTitle.includes(searchTerm) || 
 | 
			
		||||
                                   itemDescription.includes(searchTerm);
 | 
			
		||||
 | 
			
		||||
                if (assetTypeMatch && listingTypeMatch && priceMatch && searchMatch) {
 | 
			
		||||
                    item.style.display = '';
 | 
			
		||||
                } else {
 | 
			
		||||
                    item.style.display = 'none';
 | 
			
		||||
                }
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
    });
 | 
			
		||||
</script>
 | 
			
		||||
{% endblock %}
 | 
			
		||||
							
								
								
									
										238
									
								
								actix_mvc_app/src/views/marketplace/my_listings.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										238
									
								
								actix_mvc_app/src/views/marketplace/my_listings.html
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,238 @@
 | 
			
		||||
{% extends "base.html" %}
 | 
			
		||||
 | 
			
		||||
{% block title %}My Marketplace Listings{% endblock %}
 | 
			
		||||
 | 
			
		||||
{% block content %}
 | 
			
		||||
<div class="container-fluid px-4">
 | 
			
		||||
    <h1 class="mt-4">My Listings</h1>
 | 
			
		||||
    <ol class="breadcrumb mb-4">
 | 
			
		||||
        <li class="breadcrumb-item"><a href="/">Home</a></li>
 | 
			
		||||
        <li class="breadcrumb-item"><a href="/marketplace">Marketplace</a></li>
 | 
			
		||||
        <li class="breadcrumb-item active">My Listings</li>
 | 
			
		||||
    </ol>
 | 
			
		||||
 | 
			
		||||
    <div class="row mb-3">
 | 
			
		||||
        <div class="col-12">
 | 
			
		||||
            <a href="/marketplace/create" class="btn btn-success">
 | 
			
		||||
                <i class="bi bi-plus-circle"></i> Create New Listing
 | 
			
		||||
            </a>
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    <!-- Listings Table -->
 | 
			
		||||
    <div class="card mb-4">
 | 
			
		||||
        <div class="card-header">
 | 
			
		||||
            <i class="bi bi-list-ul"></i>
 | 
			
		||||
            My Listings
 | 
			
		||||
        </div>
 | 
			
		||||
        <div class="card-body">
 | 
			
		||||
            <div class="table-responsive">
 | 
			
		||||
                <table class="table table-bordered table-hover">
 | 
			
		||||
                    <thead>
 | 
			
		||||
                        <tr>
 | 
			
		||||
                            <th>Asset</th>
 | 
			
		||||
                            <th>Title</th>
 | 
			
		||||
                            <th>Price</th>
 | 
			
		||||
                            <th>Type</th>
 | 
			
		||||
                            <th>Status</th>
 | 
			
		||||
                            <th>Created</th>
 | 
			
		||||
                            <th>Expires</th>
 | 
			
		||||
                            <th>Views</th>
 | 
			
		||||
                            <th>Actions</th>
 | 
			
		||||
                        </tr>
 | 
			
		||||
                    </thead>
 | 
			
		||||
                    <tbody>
 | 
			
		||||
                        {% if listings|length > 0 %}
 | 
			
		||||
                            {% for listing in listings %}
 | 
			
		||||
                                <tr>
 | 
			
		||||
                                    <td>
 | 
			
		||||
                                        <div class="d-flex align-items-center">
 | 
			
		||||
                                            {% if listing.image_url %}
 | 
			
		||||
                                                <img src="{{ listing.image_url }}" alt="{{ listing.asset_name }}" class="me-2" style="width: 30px; height: 30px; object-fit: cover;">
 | 
			
		||||
                                            {% else %}
 | 
			
		||||
                                                <i class="bi bi-collection me-2"></i>
 | 
			
		||||
                                            {% endif %}
 | 
			
		||||
                                            {{ listing.asset_name }}
 | 
			
		||||
                                        </div>
 | 
			
		||||
                                    </td>
 | 
			
		||||
                                    <td>{{ listing.title }}</td>
 | 
			
		||||
                                    <td>${{ listing.price }}</td>
 | 
			
		||||
                                    <td>
 | 
			
		||||
                                        <span class="badge bg-primary">{{ listing.listing_type.as_str() }}</span>
 | 
			
		||||
                                    </td>
 | 
			
		||||
                                    <td>
 | 
			
		||||
                                        {% if listing.status.as_str() == "Active" %}
 | 
			
		||||
                                            <span class="badge bg-success">{{ listing.status.as_str() }}</span>
 | 
			
		||||
                                        {% elif listing.status.as_str() == "Sold" %}
 | 
			
		||||
                                            <span class="badge bg-info">{{ listing.status.as_str() }}</span>
 | 
			
		||||
                                        {% elif listing.status.as_str() == "Cancelled" %}
 | 
			
		||||
                                            <span class="badge bg-danger">{{ listing.status.as_str() }}</span>
 | 
			
		||||
                                        {% elif listing.status.as_str() == "Expired" %}
 | 
			
		||||
                                            <span class="badge bg-warning text-dark">{{ listing.status.as_str() }}</span>
 | 
			
		||||
                                        {% endif %}
 | 
			
		||||
                                    </td>
 | 
			
		||||
                                    <td>{{ listing.created_at|date }}</td>
 | 
			
		||||
                                    <td>
 | 
			
		||||
                                        {% if listing.expires_at %}
 | 
			
		||||
                                            {{ listing.expires_at|date }}
 | 
			
		||||
                                        {% else %}
 | 
			
		||||
                                            <span class="text-muted">N/A</span>
 | 
			
		||||
                                        {% endif %}
 | 
			
		||||
                                    </td>
 | 
			
		||||
                                    <td>{{ listing.views }}</td>
 | 
			
		||||
                                    <td>
 | 
			
		||||
                                        <div class="btn-group" role="group">
 | 
			
		||||
                                            <a href="/marketplace/{{ listing.id }}" class="btn btn-sm btn-outline-primary">
 | 
			
		||||
                                                <i class="bi bi-eye"></i>
 | 
			
		||||
                                            </a>
 | 
			
		||||
                                            {% if listing.status.as_str() == "Active" %}
 | 
			
		||||
                                                <form action="/marketplace/{{ listing.id }}/cancel" method="post" class="d-inline">
 | 
			
		||||
                                                    <button type="submit" class="btn btn-sm btn-outline-danger" onclick="return confirm('Are you sure you want to cancel this listing?')">
 | 
			
		||||
                                                        <i class="bi bi-x-circle"></i>
 | 
			
		||||
                                                    </button>
 | 
			
		||||
                                                </form>
 | 
			
		||||
                                            {% endif %}
 | 
			
		||||
                                        </div>
 | 
			
		||||
                                    </td>
 | 
			
		||||
                                </tr>
 | 
			
		||||
                            {% endfor %}
 | 
			
		||||
                        {% else %}
 | 
			
		||||
                            <tr>
 | 
			
		||||
                                <td colspan="9" class="text-center">
 | 
			
		||||
                                    You don't have any listings yet. 
 | 
			
		||||
                                    <a href="/marketplace/create">Create your first listing</a>
 | 
			
		||||
                                </td>
 | 
			
		||||
                            </tr>
 | 
			
		||||
                        {% endif %}
 | 
			
		||||
                    </tbody>
 | 
			
		||||
                </table>
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    <!-- Listing Statistics -->
 | 
			
		||||
    <div class="row">
 | 
			
		||||
        <div class="col-lg-6">
 | 
			
		||||
            <div class="card mb-4">
 | 
			
		||||
                <div class="card-header">
 | 
			
		||||
                    <i class="bi bi-bar-chart"></i>
 | 
			
		||||
                    Listings by Status
 | 
			
		||||
                </div>
 | 
			
		||||
                <div class="card-body">
 | 
			
		||||
                    <canvas id="statusChart" width="100%" height="50"></canvas>
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div class="col-lg-6">
 | 
			
		||||
            <div class="card mb-4">
 | 
			
		||||
                <div class="card-header">
 | 
			
		||||
                    <i class="bi bi-pie-chart"></i>
 | 
			
		||||
                    Listings by Type
 | 
			
		||||
                </div>
 | 
			
		||||
                <div class="card-body">
 | 
			
		||||
                    <canvas id="typeChart" width="100%" height="50"></canvas>
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
</div>
 | 
			
		||||
{% endblock %}
 | 
			
		||||
 | 
			
		||||
{% block scripts %}
 | 
			
		||||
<script src="https://cdn.jsdelivr.net/npm/chart.js@3.7.1/dist/chart.min.js"></script>
 | 
			
		||||
<script>
 | 
			
		||||
    document.addEventListener('DOMContentLoaded', function() {
 | 
			
		||||
        // Count listings by status
 | 
			
		||||
        const listingsData = JSON.parse('{{ listings|tojson|safe }}');
 | 
			
		||||
        
 | 
			
		||||
        const statusCounts = {
 | 
			
		||||
            'Active': 0,
 | 
			
		||||
            'Sold': 0,
 | 
			
		||||
            'Cancelled': 0,
 | 
			
		||||
            'Expired': 0
 | 
			
		||||
        };
 | 
			
		||||
        
 | 
			
		||||
        const typeCounts = {
 | 
			
		||||
            'Fixed Price': 0,
 | 
			
		||||
            'Auction': 0,
 | 
			
		||||
            'Exchange': 0
 | 
			
		||||
        };
 | 
			
		||||
        
 | 
			
		||||
        listingsData.forEach(listing => {
 | 
			
		||||
            statusCounts[listing.status] += 1;
 | 
			
		||||
            typeCounts[listing.listing_type] += 1;
 | 
			
		||||
        });
 | 
			
		||||
        
 | 
			
		||||
        // Status Chart
 | 
			
		||||
        const statusCtx = document.getElementById('statusChart').getContext('2d');
 | 
			
		||||
        new Chart(statusCtx, {
 | 
			
		||||
            type: 'bar',
 | 
			
		||||
            data: {
 | 
			
		||||
                labels: Object.keys(statusCounts),
 | 
			
		||||
                datasets: [{
 | 
			
		||||
                    label: 'Number of Listings',
 | 
			
		||||
                    data: Object.values(statusCounts),
 | 
			
		||||
                    backgroundColor: [
 | 
			
		||||
                        'rgba(40, 167, 69, 0.7)',  // Active - green
 | 
			
		||||
                        'rgba(23, 162, 184, 0.7)', // Sold - cyan
 | 
			
		||||
                        'rgba(220, 53, 69, 0.7)',  // Cancelled - red
 | 
			
		||||
                        'rgba(255, 193, 7, 0.7)'   // Expired - yellow
 | 
			
		||||
                    ],
 | 
			
		||||
                    borderColor: [
 | 
			
		||||
                        'rgba(40, 167, 69, 1)',
 | 
			
		||||
                        'rgba(23, 162, 184, 1)',
 | 
			
		||||
                        'rgba(220, 53, 69, 1)',
 | 
			
		||||
                        'rgba(255, 193, 7, 1)'
 | 
			
		||||
                    ],
 | 
			
		||||
                    borderWidth: 1
 | 
			
		||||
                }]
 | 
			
		||||
            },
 | 
			
		||||
            options: {
 | 
			
		||||
                scales: {
 | 
			
		||||
                    y: {
 | 
			
		||||
                        beginAtZero: true,
 | 
			
		||||
                        ticks: {
 | 
			
		||||
                            precision: 0
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                },
 | 
			
		||||
                plugins: {
 | 
			
		||||
                    legend: {
 | 
			
		||||
                        display: false
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
        
 | 
			
		||||
        // Type Chart
 | 
			
		||||
        const typeCtx = document.getElementById('typeChart').getContext('2d');
 | 
			
		||||
        new Chart(typeCtx, {
 | 
			
		||||
            type: 'pie',
 | 
			
		||||
            data: {
 | 
			
		||||
                labels: Object.keys(typeCounts),
 | 
			
		||||
                datasets: [{
 | 
			
		||||
                    data: Object.values(typeCounts),
 | 
			
		||||
                    backgroundColor: [
 | 
			
		||||
                        'rgba(0, 123, 255, 0.7)',  // Fixed Price - blue
 | 
			
		||||
                        'rgba(111, 66, 193, 0.7)', // Auction - purple
 | 
			
		||||
                        'rgba(23, 162, 184, 0.7)'  // Exchange - cyan
 | 
			
		||||
                    ],
 | 
			
		||||
                    borderColor: [
 | 
			
		||||
                        'rgba(0, 123, 255, 1)',
 | 
			
		||||
                        'rgba(111, 66, 193, 1)',
 | 
			
		||||
                        'rgba(23, 162, 184, 1)'
 | 
			
		||||
                    ],
 | 
			
		||||
                    borderWidth: 1
 | 
			
		||||
                }]
 | 
			
		||||
            },
 | 
			
		||||
            options: {
 | 
			
		||||
                plugins: {
 | 
			
		||||
                    legend: {
 | 
			
		||||
                        position: 'right'
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
    });
 | 
			
		||||
</script>
 | 
			
		||||
{% endblock %}
 | 
			
		||||
		Reference in New Issue
	
	Block a user