initial commit
This commit is contained in:
1
marketplace/.gitignore
vendored
Normal file
1
marketplace/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/target
|
1319
marketplace/Cargo.lock
generated
Normal file
1319
marketplace/Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
37
marketplace/Cargo.toml
Normal file
37
marketplace/Cargo.toml
Normal file
@@ -0,0 +1,37 @@
|
||||
[package]
|
||||
name = "marketplace"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[workspace]
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib"]
|
||||
|
||||
[dependencies]
|
||||
yew = { version = "0.21", features = ["csr"] }
|
||||
web-sys = { version = "0.3", features = [
|
||||
"console",
|
||||
"Document",
|
||||
"Element",
|
||||
"HtmlElement",
|
||||
"HtmlInputElement",
|
||||
"HtmlSelectElement",
|
||||
"Location",
|
||||
"Window",
|
||||
"History",
|
||||
"MouseEvent",
|
||||
"Event",
|
||||
"EventTarget",
|
||||
"Storage",
|
||||
"UrlSearchParams"
|
||||
] }
|
||||
wasm-bindgen = "0.2"
|
||||
log = "0.4"
|
||||
wasm-logger = "0.2"
|
||||
gloo = { version = "0.10", features = ["storage", "timers", "events"] }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
|
||||
[dev-dependencies]
|
||||
wasm-bindgen-test = "0.3"
|
11
marketplace/Trunk.toml
Normal file
11
marketplace/Trunk.toml
Normal file
@@ -0,0 +1,11 @@
|
||||
[build]
|
||||
target = "index.html"
|
||||
dist = "dist"
|
||||
|
||||
[watch]
|
||||
watch = ["src", "index.html", "static"]
|
||||
|
||||
[serve]
|
||||
address = "127.0.0.1"
|
||||
port = 8080
|
||||
open = false
|
50
marketplace/index.html
Normal file
50
marketplace/index.html
Normal file
@@ -0,0 +1,50 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" data-bs-theme="light">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Zanzibar Digital Freezone Marketplace</title>
|
||||
|
||||
<!-- Google Fonts -->
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&display=swap" rel="stylesheet">
|
||||
|
||||
<!-- Bootstrap CSS -->
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
|
||||
<!-- Bootstrap Icons -->
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.css" rel="stylesheet">
|
||||
|
||||
<!-- Custom CSS -->
|
||||
<link rel="stylesheet" href="/static/style.css">
|
||||
<link data-trunk rel="css" href="static/css/main.css">
|
||||
|
||||
<!-- Favicon -->
|
||||
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>💎</text></svg>">
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
|
||||
<!-- Bootstrap JS -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
|
||||
|
||||
<!-- Theme Toggle Script -->
|
||||
<script>
|
||||
// Theme toggle functionality
|
||||
function toggleTheme() {
|
||||
const html = document.documentElement;
|
||||
const currentTheme = html.getAttribute('data-bs-theme');
|
||||
const newTheme = currentTheme === 'dark' ? 'light' : 'dark';
|
||||
html.setAttribute('data-bs-theme', newTheme);
|
||||
localStorage.setItem('theme', newTheme);
|
||||
}
|
||||
|
||||
// Load saved theme
|
||||
const savedTheme = localStorage.getItem('theme');
|
||||
if (savedTheme) {
|
||||
document.documentElement.setAttribute('data-bs-theme', savedTheme);
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
77
marketplace/src/app.rs
Normal file
77
marketplace/src/app.rs
Normal file
@@ -0,0 +1,77 @@
|
||||
use yew::prelude::*;
|
||||
use crate::routing::{AppView, HistoryManager};
|
||||
use crate::components::{Header, Footer};
|
||||
use crate::views::{CreateListingView, EditListingView, HomeView, ListingDetailView, MyListingsView};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum Msg {
|
||||
SwitchView(AppView),
|
||||
PopStateChanged,
|
||||
}
|
||||
|
||||
pub struct App {
|
||||
history_manager: HistoryManager,
|
||||
current_view: AppView,
|
||||
}
|
||||
|
||||
impl Component for App {
|
||||
type Message = Msg;
|
||||
type Properties = ();
|
||||
|
||||
fn create(ctx: &Context<Self>) -> Self {
|
||||
let history_manager = HistoryManager::new(ctx.link().callback(|_| Msg::PopStateChanged));
|
||||
let current_view = history_manager.current_view();
|
||||
|
||||
Self {
|
||||
history_manager,
|
||||
current_view,
|
||||
}
|
||||
}
|
||||
|
||||
fn update(&mut self, _ctx: &Context<Self>, msg: Self::Message) -> bool {
|
||||
match msg {
|
||||
Msg::SwitchView(view) => {
|
||||
if self.current_view != view {
|
||||
self.history_manager.push_state(&view);
|
||||
self.current_view = view;
|
||||
return true;
|
||||
}
|
||||
false
|
||||
}
|
||||
Msg::PopStateChanged => {
|
||||
let new_view = self.history_manager.current_view();
|
||||
if self.current_view != new_view {
|
||||
self.current_view = new_view;
|
||||
return true;
|
||||
}
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn view(&self, ctx: &Context<Self>) -> Html {
|
||||
html! {
|
||||
<div class="d-flex flex-column min-vh-100">
|
||||
<Header on_view_change={ctx.link().callback(Msg::SwitchView)} />
|
||||
<main class="flex-grow-1">
|
||||
{ self.render_current_view(ctx) }
|
||||
</main>
|
||||
<Footer />
|
||||
</div>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl App {
|
||||
fn render_current_view(&self, ctx: &Context<Self>) -> Html {
|
||||
match &self.current_view {
|
||||
AppView::Home | AppView::Browse => html! { <HomeView on_view_change={ctx.link().callback(Msg::SwitchView)} /> },
|
||||
AppView::AssetDetail(id) => html! { <ListingDetailView id={id.clone()} /> },
|
||||
AppView::MyListings => html! { <MyListingsView /> },
|
||||
AppView::Purchases => html! { <MyListingsView /> }, // Placeholder for now
|
||||
AppView::CreateListing => html! { <CreateListingView /> },
|
||||
AppView::EditListing(id) => html! { <EditListingView id={id.clone()} /> },
|
||||
_ => html!{ <HomeView on_view_change={ctx.link().callback(Msg::SwitchView)} /> } // Default to home
|
||||
}
|
||||
}
|
||||
}
|
34
marketplace/src/components/footer.rs
Normal file
34
marketplace/src/components/footer.rs
Normal file
@@ -0,0 +1,34 @@
|
||||
use yew::prelude::*;
|
||||
|
||||
#[function_component(Footer)]
|
||||
pub fn footer() -> Html {
|
||||
html! {
|
||||
<footer class="footer mt-auto py-4 border-top">
|
||||
<div class="container-fluid px-4">
|
||||
<div class="row align-items-center">
|
||||
<div class="col-md-6">
|
||||
<div class="d-flex align-items-center">
|
||||
<i class="bi bi-gem me-2 text-primary fs-5"></i>
|
||||
<span class="fw-semibold text-primary">{ "Zanzibar Digital Freezone" }</span>
|
||||
</div>
|
||||
<p class="text-muted mb-0 mt-1">{ "Trade tokenized real-world assets, NFTs, and crypto on blockchain" }</p>
|
||||
</div>
|
||||
<div class="col-md-6 text-md-end mt-3 mt-md-0">
|
||||
<div class="d-flex justify-content-md-end justify-content-start align-items-center">
|
||||
<a href="#" class="text-muted me-3 text-decoration-none">
|
||||
<i class="bi bi-shield-check me-1"></i>{ "Security" }
|
||||
</a>
|
||||
<a href="#" class="text-muted me-3 text-decoration-none">
|
||||
<i class="bi bi-question-circle me-1"></i>{ "Help" }
|
||||
</a>
|
||||
<a href="#" class="text-muted text-decoration-none">
|
||||
<i class="bi bi-file-text me-1"></i>{ "Terms" }
|
||||
</a>
|
||||
</div>
|
||||
<p class="text-muted mb-0 mt-2 small">{ "© 2025 Zanzibar Digital Marketplace. All rights reserved." }</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
}
|
||||
}
|
98
marketplace/src/components/header.rs
Normal file
98
marketplace/src/components/header.rs
Normal file
@@ -0,0 +1,98 @@
|
||||
use yew::prelude::*;
|
||||
use crate::routing::AppView;
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
#[wasm_bindgen]
|
||||
extern "C" {
|
||||
#[wasm_bindgen(js_namespace = window)]
|
||||
fn toggleTheme();
|
||||
}
|
||||
|
||||
#[derive(Properties, PartialEq)]
|
||||
pub struct HeaderProps {
|
||||
pub on_view_change: Callback<AppView>,
|
||||
}
|
||||
|
||||
#[function_component(Header)]
|
||||
pub fn header(props: &HeaderProps) -> Html {
|
||||
let on_nav_click = |view: AppView| {
|
||||
let on_view_change = props.on_view_change.clone();
|
||||
Callback::from(move |_| on_view_change.emit(view.clone()))
|
||||
};
|
||||
|
||||
html! {
|
||||
<header class="navbar navbar-expand-lg shadow-sm py-3">
|
||||
<div class="container-fluid px-4">
|
||||
<a class="navbar-brand fw-bold d-flex align-items-center" href="#" onclick={on_nav_click(AppView::Home)}>
|
||||
<i class="bi bi-gem me-2 fs-3 text-primary"></i>
|
||||
<span class="text-primary">{ "Zanzibar Digital Freezone" }</span>
|
||||
</a>
|
||||
|
||||
<button class="navbar-toggler border-0" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
|
||||
<div class="collapse navbar-collapse" id="navbarNav">
|
||||
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link active fw-semibold" aria-current="page" href="#" onclick={on_nav_click(AppView::Home)}>
|
||||
<i class="bi bi-shop me-1"></i>
|
||||
{ "Marketplace" }
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link fw-semibold" href="#" onclick={on_nav_click(AppView::Browse)}>
|
||||
<i class="bi bi-grid-3x3-gap me-1"></i>
|
||||
{ "Browse" }
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<ul class="navbar-nav ms-auto d-flex align-items-center">
|
||||
<li class="nav-item me-3">
|
||||
<button class="btn btn-outline-secondary btn-sm rounded-pill"
|
||||
onclick={Callback::from(|_| toggleTheme())}
|
||||
title="Toggle theme">
|
||||
<i class="bi bi-moon-stars"></i>
|
||||
</button>
|
||||
</li>
|
||||
<li class="nav-item me-3">
|
||||
<a class="nav-link position-relative" href="#" onclick={on_nav_click(AppView::Purchases)} title="My Cart">
|
||||
<i class="bi bi-cart3 fs-5"></i>
|
||||
<span class="position-absolute top-0 start-100 translate-middle badge rounded-pill bg-danger" style="font-size: 0.6rem;">
|
||||
{ "2" }
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item dropdown">
|
||||
<a class="nav-link dropdown-toggle d-flex align-items-center fw-semibold" href="#" id="navbarDropdown" role="button" data-bs-toggle="dropdown" aria-expanded="false">
|
||||
<div class="rounded-circle bg-primary d-flex align-items-center justify-content-center me-2" style="width: 32px; height: 32px;">
|
||||
<i class="bi bi-person-fill text-white"></i>
|
||||
</div>
|
||||
{ "Profile" }
|
||||
</a>
|
||||
<ul class="dropdown-menu dropdown-menu-end shadow-lg border-0" aria-labelledby="navbarDropdown" style="border-radius: 12px; min-width: 200px;">
|
||||
<li><a class="dropdown-item py-2" href="#" onclick={on_nav_click(AppView::MyListings)}>
|
||||
<i class="bi bi-list-ul me-2"></i>{ "My Listings" }
|
||||
</a></li>
|
||||
<li><a class="dropdown-item py-2" href="#" onclick={on_nav_click(AppView::CreateListing)}>
|
||||
<i class="bi bi-plus-circle me-2"></i>{ "Create Listing" }
|
||||
</a></li>
|
||||
<li><a class="dropdown-item py-2" href="#" onclick={on_nav_click(AppView::Purchases)}>
|
||||
<i class="bi bi-bag-check me-2"></i>{ "My Purchases" }
|
||||
</a></li>
|
||||
<li><hr class="dropdown-divider my-2" /></li>
|
||||
<li><a class="dropdown-item py-2" href="#">
|
||||
<i class="bi bi-gear me-2"></i>{ "Settings" }
|
||||
</a></li>
|
||||
<li><a class="dropdown-item py-2 text-danger" href="#">
|
||||
<i class="bi bi-box-arrow-right me-2"></i>{ "Logout" }
|
||||
</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
}
|
||||
}
|
7
marketplace/src/components/mod.rs
Normal file
7
marketplace/src/components/mod.rs
Normal file
@@ -0,0 +1,7 @@
|
||||
pub mod footer;
|
||||
pub mod header;
|
||||
pub mod sidebar;
|
||||
|
||||
pub use footer::Footer;
|
||||
pub use header::Header;
|
||||
|
74
marketplace/src/components/sidebar.rs
Normal file
74
marketplace/src/components/sidebar.rs
Normal file
@@ -0,0 +1,74 @@
|
||||
use yew::prelude::*;
|
||||
use crate::routing::AppView;
|
||||
|
||||
#[derive(Properties, PartialEq)]
|
||||
pub struct SidebarProps {
|
||||
pub current_view: AppView,
|
||||
pub is_visible: bool,
|
||||
pub on_view_change: Callback<AppView>,
|
||||
}
|
||||
|
||||
#[function_component(Sidebar)]
|
||||
pub fn sidebar(props: &SidebarProps) -> Html {
|
||||
let class = if props.is_visible { "sidebar-visible" } else { "" };
|
||||
let on_view_change = props.on_view_change.clone();
|
||||
|
||||
let view_changer = move |view: AppView| {
|
||||
let on_view_change = on_view_change.clone();
|
||||
Callback::from(move |_| {
|
||||
on_view_change.emit(view.clone());
|
||||
})
|
||||
};
|
||||
|
||||
let home_active = if props.current_view == AppView::Home { "active" } else { "" };
|
||||
let browse_active = if matches!(props.current_view, AppView::Browse | AppView::AssetDetail(_)) { "active" } else { "" };
|
||||
let listings_active = if props.current_view == AppView::MyListings { "active" } else { "" };
|
||||
let purchases_active = if props.current_view == AppView::Purchases { "active" } else { "" };
|
||||
let watchlist_active = if props.current_view == AppView::Watchlist { "active" } else { "" };
|
||||
let create_listing_active = if props.current_view == AppView::CreateListing { "active" } else { "" };
|
||||
|
||||
html! {
|
||||
<nav id="sidebarMenu" class={classes!("col-md-3", "col-lg-2", "d-md-block", "bg-dark", "sidebar", "collapse", class)}>
|
||||
<div class="position-sticky pt-3">
|
||||
<ul class="nav flex-column">
|
||||
<li class="nav-item">
|
||||
<a class={classes!("nav-link", home_active)}
|
||||
href="#" onclick={view_changer(AppView::Home)}>
|
||||
{ "Home" }
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class={classes!("nav-link", browse_active)}
|
||||
href="#" onclick={view_changer(AppView::Browse)}>
|
||||
{ "Browse Marketplace" }
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class={classes!("nav-link", listings_active)}
|
||||
href="#" onclick={view_changer(AppView::MyListings)}>
|
||||
{ "My Listings" }
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class={classes!("nav-link", create_listing_active)}
|
||||
href="#" onclick={view_changer(AppView::CreateListing)}>
|
||||
{ "Create Listing" }
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class={classes!("nav-link", purchases_active)}
|
||||
href="#" onclick={view_changer(AppView::Purchases)}>
|
||||
{ "My Purchases" }
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class={classes!("nav-link", watchlist_active)}
|
||||
href="#" onclick={view_changer(AppView::Watchlist)}>
|
||||
{ "Watchlist" }
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
||||
}
|
||||
}
|
13
marketplace/src/lib.rs
Normal file
13
marketplace/src/lib.rs
Normal file
@@ -0,0 +1,13 @@
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
mod app;
|
||||
mod components;
|
||||
mod routing;
|
||||
mod views;
|
||||
|
||||
use app::App;
|
||||
|
||||
#[wasm_bindgen(start)]
|
||||
pub fn run_app() {
|
||||
yew::Renderer::<App>::new().render();
|
||||
}
|
86
marketplace/src/routing.rs
Normal file
86
marketplace/src/routing.rs
Normal file
@@ -0,0 +1,86 @@
|
||||
use yew::Callback;
|
||||
use wasm_bindgen::prelude::*;
|
||||
use wasm_bindgen::JsCast;
|
||||
|
||||
#[derive(Clone, PartialEq, Debug)]
|
||||
pub enum AppView {
|
||||
Home,
|
||||
Browse,
|
||||
AssetDetail(String),
|
||||
MyListings,
|
||||
Purchases,
|
||||
Watchlist,
|
||||
CreateListing,
|
||||
EditListing(String),
|
||||
}
|
||||
|
||||
impl AppView {
|
||||
pub fn to_path(&self) -> String {
|
||||
match self {
|
||||
AppView::Home => "/".to_string(),
|
||||
AppView::Browse => "/browse".to_string(),
|
||||
AppView::AssetDetail(id) => format!("/asset/{}", id),
|
||||
AppView::MyListings => "/my-listings".to_string(),
|
||||
AppView::Purchases => "/my-purchases".to_string(),
|
||||
AppView::Watchlist => "/my-watchlist".to_string(),
|
||||
AppView::CreateListing => "/create-listing".to_string(),
|
||||
AppView::EditListing(id) => format!("/edit-listing/{}", id),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_path(path: &str) -> Self {
|
||||
if path.starts_with("/asset/") {
|
||||
let id = path.trim_start_matches("/asset/").to_string();
|
||||
return AppView::AssetDetail(id);
|
||||
} else if path.starts_with("/edit-listing/") {
|
||||
let id = path.trim_start_matches("/edit-listing/").to_string();
|
||||
return AppView::EditListing(id);
|
||||
}
|
||||
|
||||
match path {
|
||||
"/" => AppView::Home,
|
||||
"/browse" => AppView::Browse,
|
||||
"/my-listings" => AppView::MyListings,
|
||||
"/my-purchases" => AppView::Purchases,
|
||||
"/my-watchlist" => AppView::Watchlist,
|
||||
"/create-listing" => AppView::CreateListing,
|
||||
_ => AppView::Home, // Default to Home for unknown paths
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct HistoryManager {
|
||||
_closure: Closure<dyn FnMut(web_sys::Event)>,
|
||||
}
|
||||
|
||||
impl HistoryManager {
|
||||
pub fn new(on_popstate: Callback<()>) -> Self {
|
||||
let closure = Closure::wrap(Box::new(move |_: web_sys::Event| {
|
||||
on_popstate.emit(());
|
||||
}) as Box<dyn FnMut(_)>);
|
||||
|
||||
if let Some(window) = web_sys::window() {
|
||||
let _ = window.add_event_listener_with_callback("popstate", closure.as_ref().unchecked_ref());
|
||||
}
|
||||
|
||||
HistoryManager { _closure: closure }
|
||||
}
|
||||
|
||||
pub fn push_state(&self, view: &AppView) {
|
||||
let path = view.to_path();
|
||||
if let Some(window) = web_sys::window() {
|
||||
if let Ok(history) = window.history() {
|
||||
if history.push_state_with_url(&JsValue::NULL, "", Some(&path)).is_err() {
|
||||
log::error!("Could not push state for path: {}", path);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn current_view(&self) -> AppView {
|
||||
let path = web_sys::window()
|
||||
.and_then(|w| w.location().pathname().ok())
|
||||
.unwrap_or_else(|| "/".to_string());
|
||||
AppView::from_path(&path)
|
||||
}
|
||||
}
|
386
marketplace/src/views/create_listing.rs
Normal file
386
marketplace/src/views/create_listing.rs
Normal file
@@ -0,0 +1,386 @@
|
||||
use yew::prelude::*;
|
||||
use web_sys::{HtmlInputElement, HtmlSelectElement, HtmlTextAreaElement};
|
||||
|
||||
#[derive(Clone, PartialEq, Debug, Default)]
|
||||
struct ListingFormData {
|
||||
title: String,
|
||||
asset_id: String,
|
||||
description: String,
|
||||
price: String,
|
||||
currency: String,
|
||||
listing_type: String,
|
||||
duration_days: u32,
|
||||
tags: String,
|
||||
terms_agreed: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq)]
|
||||
struct Asset {
|
||||
id: u32,
|
||||
name: String,
|
||||
asset_type: String,
|
||||
image_url: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq)]
|
||||
struct SelectedAssetPreview {
|
||||
name: String,
|
||||
asset_type: String,
|
||||
image_url: String,
|
||||
}
|
||||
|
||||
pub enum Msg {
|
||||
UpdateString(String, String),
|
||||
UpdateU32(String, u32),
|
||||
UpdateBool(String, bool),
|
||||
SelectAsset(String),
|
||||
Submit,
|
||||
}
|
||||
|
||||
pub struct CreateListingView {
|
||||
form_data: ListingFormData,
|
||||
assets: Vec<Asset>,
|
||||
listing_types: Vec<String>,
|
||||
selected_asset: Option<SelectedAssetPreview>,
|
||||
}
|
||||
|
||||
impl Component for CreateListingView {
|
||||
type Message = Msg;
|
||||
type Properties = ();
|
||||
|
||||
fn create(_ctx: &Context<Self>) -> Self {
|
||||
Self {
|
||||
form_data: ListingFormData {
|
||||
duration_days: 30,
|
||||
currency: "USD".to_string(),
|
||||
listing_type: "Sale".to_string(),
|
||||
..Default::default()
|
||||
},
|
||||
assets: vec![
|
||||
Asset { id: 1, name: "Coastal Reforestation Project".to_string(), asset_type: "Carbon Credits".to_string(), image_url: "https://images.unsplash.com/photo-1441974231531-c6227db76b6e?q=80&w=400&auto=format&fit=crop&ixlib=rb-4.0.3".to_string() },
|
||||
Asset { id: 2, name: "Community Wind Farm Share".to_string(), asset_type: "Energy Cooperative".to_string(), image_url: "https://images.unsplash.com/photo-1466611653911-95081537e5b7?q=80&w=400&auto=format&fit=crop&ixlib=rb-4.0.3".to_string() },
|
||||
Asset { id: 3, name: "Organic Farm Collective".to_string(), asset_type: "Tokenized Land".to_string(), image_url: "https://images.unsplash.com/photo-1500382017468-9049fed747ef?q=80&w=400&auto=format&fit=crop&ixlib=rb-4.0.3".to_string() },
|
||||
],
|
||||
listing_types: vec!["Sale".to_string(), "Auction".to_string(), "Offer".to_string()],
|
||||
selected_asset: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn update(&mut self, _ctx: &Context<Self>, msg: Self::Message) -> bool {
|
||||
match msg {
|
||||
Msg::UpdateString(name, value) => match name.as_str() {
|
||||
"title" => self.form_data.title = value,
|
||||
"description" => self.form_data.description = value,
|
||||
"price" => self.form_data.price = value,
|
||||
"currency" => self.form_data.currency = value,
|
||||
"listing_type" => self.form_data.listing_type = value,
|
||||
"tags" => self.form_data.tags = value,
|
||||
_ => (),
|
||||
},
|
||||
Msg::UpdateU32(name, value) => {
|
||||
if name == "duration_days" {
|
||||
self.form_data.duration_days = value;
|
||||
}
|
||||
}
|
||||
Msg::UpdateBool(name, value) => {
|
||||
if name == "terms" {
|
||||
self.form_data.terms_agreed = value;
|
||||
}
|
||||
}
|
||||
Msg::SelectAsset(asset_id) => {
|
||||
self.form_data.asset_id = asset_id.clone();
|
||||
self.selected_asset = self.assets.iter().find(|a| a.id.to_string() == asset_id).map(|asset| SelectedAssetPreview {
|
||||
name: asset.name.clone(),
|
||||
asset_type: asset.asset_type.clone(),
|
||||
image_url: asset.image_url.clone(),
|
||||
});
|
||||
}
|
||||
Msg::Submit => {
|
||||
log::info!("Submitting form: {:?}", self.form_data);
|
||||
return false; // Prevent re-render on submit for now
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
fn view(&self, ctx: &Context<Self>) -> Html {
|
||||
let link = ctx.link();
|
||||
|
||||
let on_input = |name: String| link.callback(move |e: InputEvent| {
|
||||
let target = e.target_unchecked_into::<HtmlInputElement>();
|
||||
Msg::UpdateString(name.clone(), target.value())
|
||||
});
|
||||
|
||||
let on_textarea_input = |name: String| link.callback(move |e: InputEvent| {
|
||||
let target = e.target_unchecked_into::<HtmlTextAreaElement>();
|
||||
Msg::UpdateString(name.clone(), target.value())
|
||||
});
|
||||
|
||||
let on_select_change = |name: String| link.callback(move |e: Event| {
|
||||
let target = e.target_unchecked_into::<HtmlSelectElement>();
|
||||
Msg::UpdateString(name.clone(), target.value())
|
||||
});
|
||||
|
||||
let on_asset_select = link.callback(|e: Event| {
|
||||
let target = e.target_unchecked_into::<HtmlSelectElement>();
|
||||
Msg::SelectAsset(target.value())
|
||||
});
|
||||
|
||||
let on_checkbox_change = |name: String| link.callback(move |e: Event| {
|
||||
let target = e.target_unchecked_into::<HtmlInputElement>();
|
||||
Msg::UpdateBool(name.clone(), target.checked())
|
||||
});
|
||||
|
||||
let _on_number_input = |name: String| link.callback(move |e: InputEvent| {
|
||||
let target = e.target_unchecked_into::<HtmlInputElement>();
|
||||
Msg::UpdateU32(name.clone(), target.value_as_number() as u32)
|
||||
});
|
||||
|
||||
let on_submit = link.callback(|e: SubmitEvent| {
|
||||
e.prevent_default();
|
||||
Msg::Submit
|
||||
});
|
||||
|
||||
html! {
|
||||
<div class="fade-in">
|
||||
<div class="container-fluid px-4 py-4">
|
||||
<nav aria-label="breadcrumb" class="mb-4">
|
||||
<ol class="breadcrumb">
|
||||
<li class="breadcrumb-item"><a href="/">{ "Home" }</a></li>
|
||||
<li class="breadcrumb-item"><a href="/browse">{ "Marketplace" }</a></li>
|
||||
<li class="breadcrumb-item active" aria-current="page">{ "Create Listing" }</li>
|
||||
</ol>
|
||||
</nav>
|
||||
|
||||
<div class="mb-4">
|
||||
<h1 class="h2 fw-bold mb-2">{ "Create New Listing" }</h1>
|
||||
<p class="text-muted">{ "List your digital asset for sale or auction" }</p>
|
||||
</div>
|
||||
|
||||
<div class="row g-4">
|
||||
<div class="col-lg-8">
|
||||
<div class="card border-0 shadow-lg">
|
||||
<div class="card-header bg-transparent border-0 p-4">
|
||||
<h5 class="fw-bold mb-0">
|
||||
<i class="bi bi-plus-circle me-2 text-primary"></i>
|
||||
{ "Listing Details" }
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body p-4 pt-0">
|
||||
<form onsubmit={on_submit}>
|
||||
<div class="mb-4">
|
||||
<label for="title" class="form-label">{ "Listing Title" }</label>
|
||||
<input type="text" class="form-control" id="title" required=true
|
||||
placeholder="Enter a compelling title for your asset"
|
||||
oninput={on_input("title".to_string())} />
|
||||
<div class="form-text">{ "Make it descriptive and eye-catching" }</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<label for="asset_id" class="form-label">{ "Select Asset" }</label>
|
||||
<select class="form-select" id="asset_id" required=true onchange={on_asset_select}>
|
||||
<option value="" selected=true disabled=true>{ "Choose an asset to list" }</option>
|
||||
{ for self.assets.iter().map(|asset| html! {
|
||||
<option value={asset.id.to_string()}>
|
||||
{ format!("{} ({})", asset.name, asset.asset_type) }
|
||||
</option>
|
||||
}) }
|
||||
</select>
|
||||
<div class="form-text">{ "Select from your owned digital assets" }</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<label for="description" class="form-label">{ "Description" }</label>
|
||||
<textarea class="form-control" id="description" rows="5" required=true
|
||||
placeholder="Describe your asset in detail. Include its features, rarity, and any special attributes..."
|
||||
oninput={on_textarea_input("description".to_string())}></textarea>
|
||||
<div class="form-text">{ "Provide detailed information to attract buyers" }</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-3 mb-4">
|
||||
<div class="col-md-6">
|
||||
<label for="price" class="form-label">{ "Starting Price" }</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text bg-primary text-white">{ "$" }</span>
|
||||
<input type="number" class="form-control" id="price" required=true
|
||||
step="0.01" min="0.01" placeholder="0.00"
|
||||
oninput={on_input("price".to_string())} />
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label for="currency" class="form-label">{ "Currency" }</label>
|
||||
<select class="form-select" id="currency" required=true onchange={on_select_change("currency".to_string())}>
|
||||
<option value="USD" selected={self.form_data.currency == "USD"}>{ "USD ($)" }</option>
|
||||
<option value="EUR" selected={self.form_data.currency == "EUR"}>{ "EUR (€)" }</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-3 mb-4">
|
||||
<div class="col-md-6">
|
||||
<label for="listing_type" class="form-label">{ "Listing Type" }</label>
|
||||
<select class="form-select" id="listing_type" required=true onchange={on_select_change("listing_type".to_string())}>
|
||||
{ for self.listing_types.iter().map(|l_type| html! {
|
||||
<option value={l_type.clone()} selected={self.form_data.listing_type == *l_type}>
|
||||
{ match l_type.as_str() {
|
||||
"Sale" => "Fixed Price Sale",
|
||||
"Auction" => "Auction (Bidding)",
|
||||
"Offer" => "Accept Offers",
|
||||
_ => l_type
|
||||
}}
|
||||
</option>
|
||||
}) }
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label for="duration_days" class="form-label">{ "Duration (Days)" }</label>
|
||||
<select class="form-select" id="duration_days" onchange={on_select_change("duration_days".to_string())}>
|
||||
<option value="7" selected={self.form_data.duration_days == 7}>{ "7 days" }</option>
|
||||
<option value="14" selected={self.form_data.duration_days == 14}>{ "14 days" }</option>
|
||||
<option value="30" selected={self.form_data.duration_days == 30}>{ "30 days" }</option>
|
||||
<option value="60" selected={self.form_data.duration_days == 60}>{ "60 days" }</option>
|
||||
<option value="90" selected={self.form_data.duration_days == 90}>{ "90 days" }</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<label for="tags" class="form-label">{ "Tags" }</label>
|
||||
<input type="text" class="form-control" id="tags"
|
||||
placeholder="rare, collectible, gaming, art (separate with commas)"
|
||||
oninput={on_input("tags".to_string())} />
|
||||
<div class="form-text">{ "Add relevant tags to help buyers find your asset" }</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<div class="form-check">
|
||||
<input type="checkbox" class="form-check-input" id="terms" required=true
|
||||
onchange={on_checkbox_change("terms".to_string())} />
|
||||
<label class="form-check-label" for="terms">
|
||||
{ "I agree to the " }
|
||||
<a href="#" class="text-primary text-decoration-none">{ "marketplace terms and conditions" }</a>
|
||||
{ " and confirm that I own this digital asset" }
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-flex gap-3 justify-content-end">
|
||||
<a href="/browse" class="btn btn-outline-secondary px-4">
|
||||
<i class="bi bi-x-circle me-2"></i>{ "Cancel" }
|
||||
</a>
|
||||
<button type="submit" class="btn btn-marketplace px-4">
|
||||
<i class="bi bi-plus-circle me-2"></i>{ "Create Listing" }
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-lg-4">
|
||||
<div class="card border-0 shadow-lg mb-4">
|
||||
<div class="card-header bg-transparent border-0 p-4">
|
||||
<h6 class="fw-bold mb-0">
|
||||
<i class="bi bi-eye me-2 text-primary"></i>
|
||||
{ "Asset Preview" }
|
||||
</h6>
|
||||
</div>
|
||||
<div class="card-body text-center p-4 pt-0">
|
||||
{ self.view_asset_preview() }
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card border-0 shadow-lg mb-4">
|
||||
<div class="card-header bg-transparent border-0 p-4">
|
||||
<h6 class="fw-bold mb-0">
|
||||
<i class="bi bi-lightbulb me-2 text-primary"></i>
|
||||
{ "Listing Tips" }
|
||||
</h6>
|
||||
</div>
|
||||
<div class="card-body p-4 pt-0">
|
||||
<div class="d-flex flex-column gap-3">
|
||||
<div class="d-flex align-items-start">
|
||||
<i class="bi bi-check-circle text-success me-3 mt-1"></i>
|
||||
<div>
|
||||
<h6 class="mb-1">{ "Clear Title" }</h6>
|
||||
<small class="text-muted">{ "Use descriptive, searchable keywords" }</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex align-items-start">
|
||||
<i class="bi bi-check-circle text-success me-3 mt-1"></i>
|
||||
<div>
|
||||
<h6 class="mb-1">{ "Detailed Description" }</h6>
|
||||
<small class="text-muted">{ "Include features, rarity, and provenance" }</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex align-items-start">
|
||||
<i class="bi bi-check-circle text-success me-3 mt-1"></i>
|
||||
<div>
|
||||
<h6 class="mb-1">{ "Competitive Pricing" }</h6>
|
||||
<small class="text-muted">{ "Research similar assets for fair pricing" }</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex align-items-start">
|
||||
<i class="bi bi-check-circle text-success me-3 mt-1"></i>
|
||||
<div>
|
||||
<h6 class="mb-1">{ "Relevant Tags" }</h6>
|
||||
<small class="text-muted">{ "Help buyers discover your listing" }</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl CreateListingView {
|
||||
fn view_asset_preview(&self) -> Html {
|
||||
match &self.selected_asset {
|
||||
Some(asset) => html! {
|
||||
<div class="slide-up">
|
||||
<div class="position-relative mb-3">
|
||||
{ if !asset.image_url.is_empty() {
|
||||
html! {
|
||||
<img src={asset.image_url.clone()}
|
||||
class="img-fluid rounded-3 shadow-sm"
|
||||
alt={asset.name.clone()}
|
||||
style="max-height: 250px; width: 100%; object-fit: cover;" />
|
||||
}
|
||||
} else {
|
||||
html! {
|
||||
<div class="bg-primary-subtle d-flex align-items-center justify-content-center rounded-3 shadow-sm"
|
||||
style="height: 250px;">
|
||||
<i class="bi bi-collection text-primary" style="font-size: 4rem;"></i>
|
||||
</div>
|
||||
}
|
||||
}}
|
||||
<div class="position-absolute top-0 end-0 m-2">
|
||||
<span class="badge bg-primary-subtle text-primary px-3 py-2">{ &asset.asset_type }</span>
|
||||
</div>
|
||||
</div>
|
||||
<h5 class="fw-bold mb-2">{ &asset.name }</h5>
|
||||
<p class="text-muted small mb-3">{ "This is how your asset will appear to potential buyers in the marketplace." }</p>
|
||||
<div class="d-flex justify-content-between align-items-center p-3 bg-light rounded-3">
|
||||
<span class="text-muted small">{ "Preview Mode" }</span>
|
||||
<i class="bi bi-eye text-primary"></i>
|
||||
</div>
|
||||
</div>
|
||||
},
|
||||
None => html! {
|
||||
<div class="text-center py-5">
|
||||
<div class="bg-light d-flex align-items-center justify-content-center rounded-3 mb-4"
|
||||
style="height: 200px;">
|
||||
<i class="bi bi-image text-muted" style="font-size: 4rem;"></i>
|
||||
</div>
|
||||
<h6 class="text-muted mb-2">{ "No Asset Selected" }</h6>
|
||||
<p class="text-muted small mb-0">{ "Choose an asset from the dropdown above to see how it will appear to buyers." }</p>
|
||||
</div>
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
82
marketplace/src/views/edit_listing.rs
Normal file
82
marketplace/src/views/edit_listing.rs
Normal file
@@ -0,0 +1,82 @@
|
||||
use yew::prelude::*;
|
||||
|
||||
#[derive(Properties, PartialEq)]
|
||||
pub struct EditListingProps {
|
||||
pub id: String,
|
||||
}
|
||||
|
||||
#[function_component(EditListingView)]
|
||||
pub fn edit_listing_view(props: &EditListingProps) -> Html {
|
||||
// Mock data for an existing listing
|
||||
let listing_title = "Rare Digital Sword of the Ancients";
|
||||
let listing_price = "150.00";
|
||||
let asset_preview_url = "https://via.placeholder.com/300/007bff/fff?text=Digital+Sword";
|
||||
|
||||
html! {
|
||||
<div class="container-fluid px-4">
|
||||
<h1 class="mt-4">{ format!("Edit Listing #{}", &props.id) }</h1>
|
||||
<ol class="breadcrumb mb-4">
|
||||
<li class="breadcrumb-item"><a href="/">{ "Home" }</a></li>
|
||||
<li class="breadcrumb-item"><a href="/my-listings">{ "My Listings" }</a></li>
|
||||
<li class="breadcrumb-item active">{ "Edit Listing" }</li>
|
||||
</ol>
|
||||
|
||||
<div class="card mb-4 shadow-sm">
|
||||
<div class="card-header">
|
||||
<i class="bi bi-pencil-square me-2"></i>{ "Listing Details" }
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form>
|
||||
<div class="row">
|
||||
<div class="col-md-8">
|
||||
// Form Fields
|
||||
<div class="mb-3">
|
||||
<label for="listing-title" class="form-label">{ "Listing Title" }</label>
|
||||
<input type="text" class="form-control" id="listing-title" value={listing_title} />
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="listing-description" class="form-label">{ "Description" }</label>
|
||||
<textarea class="form-control" id="listing-description" rows="4">{ listing_description }</textarea>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="listing-price" class="form-label">{ "Price (USD)" }</label>
|
||||
<input type="number" class="form-control" id="listing-price" value={listing_price} />
|
||||
</div>
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="listing-type" class="form-label">{ "Listing Type" }</label>
|
||||
<select id="listing-type" class="form-select">
|
||||
<option>{ "Sale" }</option>
|
||||
<option selected=true>{ "Auction" }</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
// Asset Preview
|
||||
<div class="mb-3">
|
||||
<label class="form-label">{ "Asset Preview" }</label>
|
||||
<div class="card text-center">
|
||||
<div class="card-body">
|
||||
<img src={asset_preview_url} class="img-fluid rounded mb-3" alt="Asset Preview" />
|
||||
<h5>{ "Digital Sword" }</h5>
|
||||
<p class="text-muted">{ "Game Item" }</p>
|
||||
</div>
|
||||
<div class="card-footer">
|
||||
<button type="button" class="btn btn-secondary btn-sm">{ "Change Asset" }</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<hr class="my-4" />
|
||||
<div class="d-flex justify-content-end">
|
||||
<button type="button" class="btn btn-secondary me-2">{ "Cancel" }</button>
|
||||
<button type="submit" class="btn btn-primary">{ "Save Changes" }</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
254
marketplace/src/views/home.rs
Normal file
254
marketplace/src/views/home.rs
Normal file
@@ -0,0 +1,254 @@
|
||||
use yew::prelude::*;
|
||||
use crate::routing::AppView;
|
||||
use wasm_bindgen::prelude::*;
|
||||
use wasm_bindgen::JsCast;
|
||||
|
||||
#[derive(Properties, PartialEq)]
|
||||
pub struct HomeViewProps {
|
||||
pub on_view_change: Callback<AppView>,
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq)]
|
||||
struct Listing {
|
||||
id: String,
|
||||
title: String,
|
||||
description: String,
|
||||
image_url: String,
|
||||
price: f64,
|
||||
listing_type: String,
|
||||
asset_type: String,
|
||||
seller_name: String,
|
||||
}
|
||||
|
||||
#[function_component(HomeView)]
|
||||
pub fn home_view(props: &HomeViewProps) -> Html {
|
||||
let listings = use_memo((), |_| vec![
|
||||
Listing {
|
||||
id: "1".to_string(),
|
||||
title: "Permaculture Farm Token - Kilifi Coast".to_string(),
|
||||
description: "Tokenized ownership of 50 hectares of regenerative permaculture farmland on Kenya's coast. Includes carbon sequestration rights and sustainable agriculture revenue sharing.".to_string(),
|
||||
image_url: "https://images.unsplash.com/photo-1500382017468-9049fed747ef?q=80&w=2070&auto=format&fit=crop&ixlib=rb-4.0.3".to_string(),
|
||||
price: 12500.00,
|
||||
listing_type: "Sale".to_string(),
|
||||
asset_type: "Tokenized Land".to_string(),
|
||||
seller_name: "EcoFarms Collective".to_string(),
|
||||
},
|
||||
Listing {
|
||||
id: "2".to_string(),
|
||||
title: "Community Solar Cooperative Share".to_string(),
|
||||
description: "Digital ownership certificate for a community-driven solar energy cooperative. Earn dividends from clean energy production while supporting local energy independence.".to_string(),
|
||||
image_url: "https://images.unsplash.com/photo-1509391366360-2e959784a276?q=80&w=2070&auto=format&fit=crop&ixlib=rb-4.0.3".to_string(),
|
||||
price: 850.00,
|
||||
listing_type: "Auction".to_string(),
|
||||
asset_type: "Energy Cooperative".to_string(),
|
||||
seller_name: "SolarCommunity DAO".to_string(),
|
||||
},
|
||||
Listing {
|
||||
id: "3".to_string(),
|
||||
title: "Biodiversity Credits - Coastal Mangroves".to_string(),
|
||||
description: "Verified biodiversity conservation credits from protected mangrove restoration project. Each credit represents 1 hectare of preserved coastal ecosystem.".to_string(),
|
||||
image_url: "https://images.unsplash.com/photo-1559827260-dc66d52bef19?q=80&w=2070&auto=format&fit=crop&ixlib=rb-4.0.3".to_string(),
|
||||
price: 450.00,
|
||||
listing_type: "Sale".to_string(),
|
||||
asset_type: "Biodiversity Credits".to_string(),
|
||||
seller_name: "Ocean Guardians".to_string(),
|
||||
},
|
||||
Listing {
|
||||
id: "4".to_string(),
|
||||
title: "Carbon Offset Portfolio - Reforestation".to_string(),
|
||||
description: "Premium carbon offset credits from verified reforestation projects across East Africa. Each token represents 1 ton of CO2 sequestered through native tree planting.".to_string(),
|
||||
image_url: "https://images.unsplash.com/photo-1441974231531-c6227db76b6e?q=80&w=2070&auto=format&fit=crop&ixlib=rb-4.0.3".to_string(),
|
||||
price: 125.00,
|
||||
listing_type: "Sale".to_string(),
|
||||
asset_type: "Carbon Credits".to_string(),
|
||||
seller_name: "TreeFuture Initiative".to_string(),
|
||||
},
|
||||
Listing {
|
||||
id: "5".to_string(),
|
||||
title: "Rare CryptoPunk #7804".to_string(),
|
||||
description: "One of the most sought-after CryptoPunks featuring the rare alien type with cap and small shades. This iconic NFT represents early blockchain art history and digital ownership.".to_string(),
|
||||
image_url: "https://images.unsplash.com/photo-1618005182384-a83a8bd57fbe?q=80&w=2070&auto=format&fit=crop&ixlib=rb-4.0.3".to_string(),
|
||||
price: 85000.00,
|
||||
listing_type: "Sale".to_string(),
|
||||
asset_type: "NFT".to_string(),
|
||||
seller_name: "CryptoCollector".to_string(),
|
||||
},
|
||||
Listing {
|
||||
id: "6".to_string(),
|
||||
title: "DeFi Yield Farm LP Tokens".to_string(),
|
||||
description: "High-yield liquidity provider tokens from a verified DeFi protocol offering 12% APY. Backed by blue-chip crypto assets with automated compounding rewards.".to_string(),
|
||||
image_url: "https://images.unsplash.com/photo-1639762681485-074b7f938ba0?q=80&w=2070&auto=format&fit=crop&ixlib=rb-4.0.3".to_string(),
|
||||
price: 5000.00,
|
||||
listing_type: "Sale".to_string(),
|
||||
asset_type: "DeFi Token".to_string(),
|
||||
seller_name: "YieldMaster DAO".to_string(),
|
||||
},
|
||||
]);
|
||||
|
||||
let is_scrolled = use_state(|| false);
|
||||
{
|
||||
let is_scrolled = is_scrolled.clone();
|
||||
use_effect_with((), move |_| {
|
||||
let closure = Closure::wrap(Box::new(move || {
|
||||
if let Some(window) = web_sys::window() {
|
||||
is_scrolled.set(window.scroll_y().unwrap_or(0.0) > 50.0);
|
||||
}
|
||||
}) as Box<dyn FnMut()>);
|
||||
|
||||
if let Some(window) = web_sys::window() {
|
||||
window
|
||||
.add_event_listener_with_callback("scroll", closure.as_ref().unchecked_ref())
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
move || drop(closure)
|
||||
});
|
||||
}
|
||||
|
||||
let on_card_click = {
|
||||
let on_view_change = props.on_view_change.clone();
|
||||
Callback::from(move |id: String| {
|
||||
on_view_change.emit(AppView::AssetDetail(id));
|
||||
})
|
||||
};
|
||||
|
||||
let featured_listing = listings[0].clone();
|
||||
|
||||
let render_listing_card = |listing: &Listing, is_featured: bool| {
|
||||
let on_view_change = on_card_click.clone();
|
||||
let listing_id = listing.id.clone();
|
||||
let onclick = Callback::from(move |_| on_view_change.emit(listing_id.clone()));
|
||||
|
||||
html! {
|
||||
<div class={if is_featured { "col-lg-6" } else { "col" }} onclick={onclick}>
|
||||
<div class="card h-100 marketplace-card fade-in" style="cursor: pointer;">
|
||||
<div class="position-relative overflow-hidden">
|
||||
<img src={listing.image_url.clone()} class="card-img-top" alt={listing.title.clone()} style="height: 250px; object-fit: cover;" />
|
||||
<div class="position-absolute top-0 end-0 m-3">
|
||||
<span class="badge bg-primary-subtle text-primary px-3 py-2">{ &listing.asset_type }</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body d-flex flex-column p-4">
|
||||
<div class="d-flex justify-content-end align-items-start mb-2">
|
||||
<small class="text-muted">{ format!("by {}", listing.seller_name) }</small>
|
||||
</div>
|
||||
<h5 class="card-title fw-bold mb-2">{ &listing.title }</h5>
|
||||
<p class="card-text text-muted flex-grow-1 mb-3 small">{ &listing.description }</p>
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<div class="price-badge">{ format!("${:.2}", listing.price) }</div>
|
||||
<i class="bi bi-arrow-right text-primary fs-5"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
};
|
||||
|
||||
let _sticky_class = if *is_scrolled { "sticky-top" } else { "" };
|
||||
|
||||
html! {
|
||||
<div class="fade-in">
|
||||
// Hero Section
|
||||
<div class="hero-section py-5">
|
||||
<div class="container-fluid px-4">
|
||||
<div class="row align-items-center min-vh-50">
|
||||
<div class="col-lg-6 mb-4 mb-lg-0">
|
||||
<div class="slide-up">
|
||||
<h1 class="display-3 fw-bold mb-4 hero-title">
|
||||
{ "Trade " }
|
||||
<span class="text-warning">{ "Digital Assets" }</span>
|
||||
{ " & Crypto" }
|
||||
</h1>
|
||||
<p class="fs-5 mb-4 pe-lg-5 hero-subtitle">
|
||||
{ "The premier marketplace for tokenized real-world assets and digital collectibles. Trade everything from sustainable land tokens and carbon credits to NFTs, crypto assets, and blockchain-based investments." }
|
||||
</p>
|
||||
<div class="d-flex flex-wrap gap-3">
|
||||
<button class="btn btn-marketplace btn-lg">
|
||||
<i class="bi bi-search me-2"></i>{ "Explore Marketplace" }
|
||||
</button>
|
||||
<button class="btn btn-secondary btn-lg">
|
||||
<i class="bi bi-plus-circle me-2"></i>{ "List Your Asset" }
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-6">
|
||||
<div class="slide-up" style="animation-delay: 0.2s;">
|
||||
{ render_listing_card(&featured_listing, true) }
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
// Search and Filter Section
|
||||
<div class="py-5 bg-light">
|
||||
<div class="container-fluid px-4">
|
||||
<div class="search-container">
|
||||
<div class="row align-items-center mb-4">
|
||||
<div class="col">
|
||||
<h2 class="h4 fw-bold mb-0">{ "Browse Digital Assets" }</h2>
|
||||
<p class="text-muted mb-0">{ "Find exactly what you're looking for" }</p>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<span class="badge bg-primary-subtle text-primary px-3 py-2">
|
||||
{ format!("{} items available", listings.len()) }
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row g-3">
|
||||
<div class="col-lg-4 col-md-6">
|
||||
<div class="position-relative">
|
||||
<i class="bi bi-search position-absolute top-50 start-0 translate-middle-y ms-3 text-muted"></i>
|
||||
<input type="text" class="form-control ps-5" placeholder="Search by name, category, or seller..." />
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-3 col-md-6">
|
||||
<select class="form-select">
|
||||
<option selected={true}>{ "All Categories" }</option>
|
||||
<option value="land">{ "Tokenized Real Estate" }</option>
|
||||
<option value="carbon">{ "Carbon & Environmental Credits" }</option>
|
||||
<option value="energy">{ "Energy & Infrastructure" }</option>
|
||||
<option value="nft">{ "NFTs & Digital Art" }</option>
|
||||
<option value="defi">{ "DeFi & Yield Tokens" }</option>
|
||||
<option value="gaming">{ "Gaming Assets" }</option>
|
||||
<option value="collectibles">{ "Digital Collectibles" }</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-lg-3 col-md-6">
|
||||
<select class="form-select">
|
||||
<option selected={true}>{ "Sort by: Newest" }</option>
|
||||
<option value="price_low">{ "Price: Low to High" }</option>
|
||||
<option value="price_high">{ "Price: High to Low" }</option>
|
||||
<option value="popular">{ "Most Popular" }</option>
|
||||
<option value="ending">{ "Ending Soon" }</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-lg-2 col-md-6">
|
||||
<button class="btn btn-primary w-100">
|
||||
<i class="bi bi-funnel me-2"></i>{ "Apply Filters" }
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
// Listings Grid
|
||||
<div class="py-5">
|
||||
<div class="container-fluid px-4">
|
||||
<div class="row g-4">
|
||||
{ for listings.iter().skip(1).map(|l| render_listing_card(l, false)) }
|
||||
</div>
|
||||
|
||||
// Load More Section
|
||||
<div class="text-center mt-5">
|
||||
<button class="btn btn-outline-primary btn-lg">
|
||||
<i class="bi bi-arrow-down-circle me-2"></i>{ "Load More Assets" }
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
325
marketplace/src/views/listing_detail.rs
Normal file
325
marketplace/src/views/listing_detail.rs
Normal file
@@ -0,0 +1,325 @@
|
||||
use yew::prelude::*;
|
||||
|
||||
#[derive(Clone, PartialEq)]
|
||||
struct Listing {
|
||||
id: String,
|
||||
title: String,
|
||||
description: String,
|
||||
image_url: String,
|
||||
price: f64,
|
||||
currency: String,
|
||||
listing_type: String,
|
||||
status: String,
|
||||
seller_name: String,
|
||||
asset_name: String,
|
||||
asset_type: String,
|
||||
asset_id: String,
|
||||
expires_at: String,
|
||||
created_at: String,
|
||||
tags: Vec<String>,
|
||||
bids: Vec<Bid>,
|
||||
// Blockchain/Crypto fields
|
||||
blockchain: String,
|
||||
contract_address: String,
|
||||
token_id: String,
|
||||
token_standard: String,
|
||||
total_supply: u64,
|
||||
current_supply: u64,
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq)]
|
||||
struct Bid {
|
||||
bidder_name: String,
|
||||
amount: f64,
|
||||
created_at: String,
|
||||
}
|
||||
|
||||
#[derive(Properties, PartialEq)]
|
||||
pub struct ListingDetailProps {
|
||||
pub id: String,
|
||||
}
|
||||
|
||||
#[function_component(ListingDetailView)]
|
||||
pub fn listing_detail_view(props: &ListingDetailProps) -> Html {
|
||||
// Mock data - in a real app, this would be fetched based on props.id
|
||||
let listing = Listing {
|
||||
id: props.id.clone(),
|
||||
title: "Permaculture Farm Token - Kilifi Coast".to_string(),
|
||||
description: "Tokenized ownership of 50 hectares of regenerative permaculture farmland on Kenya's coast. This sustainable agriculture project includes carbon sequestration rights, biodiversity conservation benefits, and revenue sharing from organic crop production. The farm employs local communities and uses traditional ecological knowledge combined with modern permaculture techniques.".to_string(),
|
||||
image_url: "https://images.unsplash.com/photo-1500382017468-9049fed747ef?q=80&w=2070&auto=format&fit=crop&ixlib=rb-4.0.3".to_string(),
|
||||
price: 12500.00,
|
||||
currency: "USD".to_string(),
|
||||
listing_type: "Auction".to_string(),
|
||||
status: "Active".to_string(),
|
||||
seller_name: "EcoFarms Collective".to_string(),
|
||||
asset_name: "Kilifi Permaculture Farm".to_string(),
|
||||
asset_type: "Tokenized Land".to_string(),
|
||||
asset_id: "KPF-2025-001".to_string(),
|
||||
expires_at: "2025-07-15T23:59:59Z".to_string(),
|
||||
created_at: "2025-06-26T12:00:00Z".to_string(),
|
||||
tags: vec!["permaculture".to_string(), "sustainable".to_string(), "carbon-credits".to_string(), "community".to_string(), "agriculture".to_string()],
|
||||
bids: vec![
|
||||
Bid { bidder_name: "GreenInvestor".to_string(), amount: 13250.00, created_at: "2025-06-26T15:30:00Z".to_string() },
|
||||
Bid { bidder_name: "SustainableFunds".to_string(), amount: 12800.00, created_at: "2025-06-26T14:00:00Z".to_string() },
|
||||
],
|
||||
// Blockchain/Crypto fields
|
||||
blockchain: "Polygon".to_string(),
|
||||
contract_address: "0x742d35Cc6634C0532925a3b8D4C9db96c4b4d8e9".to_string(),
|
||||
token_id: "1001".to_string(),
|
||||
token_standard: "ERC-721".to_string(),
|
||||
total_supply: 100,
|
||||
current_supply: 100,
|
||||
};
|
||||
|
||||
html! {
|
||||
<div class="fade-in">
|
||||
<div class="container-fluid px-4 py-4">
|
||||
<nav aria-label="breadcrumb" class="mb-4">
|
||||
<ol class="breadcrumb">
|
||||
<li class="breadcrumb-item"><a href="/">{ "Home" }</a></li>
|
||||
<li class="breadcrumb-item"><a href="/browse">{ "Marketplace" }</a></li>
|
||||
<li class="breadcrumb-item active" aria-current="page">{ &listing.title }</li>
|
||||
</ol>
|
||||
</nav>
|
||||
|
||||
<div class="row g-4" style="height: calc(100vh - 200px);">
|
||||
// Left Column: Image and Actions
|
||||
<div class="col-lg-4">
|
||||
<div class="card border-0 shadow-lg">
|
||||
<div class="card-body p-3">
|
||||
<div class="position-relative mb-3">
|
||||
<img src={listing.image_url.clone()} alt={listing.title.clone()}
|
||||
class="asset-detail-image w-100" style="height: 250px; object-fit: cover;" />
|
||||
<div class="position-absolute top-0 end-0 m-2">
|
||||
<span class="badge bg-primary-subtle text-primary px-2 py-1">{ &listing.asset_type }</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-grid gap-2">
|
||||
{ if listing.listing_type == "Auction" {
|
||||
html! {
|
||||
<button type="button" class="btn btn-bid btn-sm">
|
||||
<i class="bi bi-hammer me-1"></i>{ "Place Bid" }
|
||||
</button>
|
||||
}
|
||||
} else {
|
||||
html! {
|
||||
<button type="button" class="btn btn-marketplace btn-sm">
|
||||
<i class="bi bi-cart-plus me-1"></i>{ "Buy Now" }
|
||||
</button>
|
||||
}
|
||||
}}
|
||||
<button type="button" class="btn btn-outline-primary btn-sm">
|
||||
<i class="bi bi-heart me-1"></i>{ "Watchlist" }
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-secondary btn-sm">
|
||||
<i class="bi bi-share me-1"></i>{ "Share" }
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
// Blockchain Details Card
|
||||
<div class="card border-0 shadow-lg mt-3">
|
||||
<div class="card-header bg-transparent border-0 p-3">
|
||||
<h6 class="fw-bold mb-0">
|
||||
<i class="bi bi-link-45deg me-2 text-primary"></i>
|
||||
{ "Blockchain Details" }
|
||||
</h6>
|
||||
</div>
|
||||
<div class="card-body p-3 pt-0" style="max-height: 200px; overflow-y: auto;">
|
||||
<div class="row g-2">
|
||||
<div class="col-6">
|
||||
<div class="d-flex align-items-center mb-2">
|
||||
<i class="bi bi-diagram-3 me-2 text-success"></i>
|
||||
<div>
|
||||
<small class="text-muted d-block">{ "Blockchain" }</small>
|
||||
<span class="fw-semibold small">{ &listing.blockchain }</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<div class="d-flex align-items-center mb-2">
|
||||
<i class="bi bi-shield-check me-2 text-info"></i>
|
||||
<div>
|
||||
<small class="text-muted d-block">{ "Standard" }</small>
|
||||
<span class="fw-semibold small">{ &listing.token_standard }</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<div class="d-flex align-items-center mb-2">
|
||||
<i class="bi bi-file-earmark-code me-2 text-warning"></i>
|
||||
<div class="flex-grow-1">
|
||||
<small class="text-muted d-block">{ "Contract" }</small>
|
||||
<code class="small bg-light px-2 py-1 rounded d-block text-truncate">{ &listing.contract_address }</code>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<small class="text-muted d-block">{ "Token ID" }</small>
|
||||
<span class="fw-semibold small">{ &listing.token_id }</span>
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<small class="text-muted d-block">{ "Supply" }</small>
|
||||
<span class="fw-semibold small">{ listing.total_supply }</span>
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<small class="text-muted d-block">{ "Available" }</small>
|
||||
<span class="fw-semibold small">{ listing.current_supply }</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-2 pt-2 border-top">
|
||||
<div class="d-flex gap-1">
|
||||
<a href="#" class="btn btn-sm btn-outline-primary flex-fill">
|
||||
<i class="bi bi-eye me-1"></i>{ "Explorer" }
|
||||
</a>
|
||||
<a href="#" class="btn btn-sm btn-outline-secondary flex-fill">
|
||||
<i class="bi bi-file-text me-1"></i>{ "Contract" }
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
// Right Column: Details
|
||||
<div class="col-lg-8">
|
||||
<div class="d-flex flex-column h-100">
|
||||
// Header with title and price
|
||||
<div class="card border-0 shadow-lg mb-3">
|
||||
<div class="card-body p-3">
|
||||
<div class="d-flex justify-content-between align-items-start mb-2">
|
||||
<div class="flex-grow-1">
|
||||
<h1 class="h4 fw-bold mb-1">{ &listing.title }</h1>
|
||||
<p class="text-muted mb-0 small">{ format!("Asset ID: {}", listing.asset_id) }</p>
|
||||
</div>
|
||||
<span class="badge bg-success px-2 py-1">{ &listing.status }</span>
|
||||
</div>
|
||||
<div class="price-badge d-inline-block">{ format!("${:.2}", listing.price) }</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
// Scrollable content area
|
||||
<div class="flex-grow-1" style="overflow-y: auto;">
|
||||
// Description Card
|
||||
<div class="card border-0 shadow-lg mb-3">
|
||||
<div class="card-header bg-transparent border-0 p-3 pb-0">
|
||||
<h6 class="fw-bold mb-0">{ "Description" }</h6>
|
||||
</div>
|
||||
<div class="card-body p-3 pt-2" style="max-height: 150px; overflow-y: auto;">
|
||||
<p class="mb-0">{ &listing.description }</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
// Details Card
|
||||
<div class="card border-0 shadow-lg mb-3">
|
||||
<div class="card-header bg-transparent border-0 p-3 pb-0">
|
||||
<h6 class="fw-bold mb-0">{ "Details" }</h6>
|
||||
</div>
|
||||
<div class="card-body p-3 pt-2">
|
||||
<div class="row g-2">
|
||||
<div class="col-6">
|
||||
<div class="d-flex align-items-center mb-2">
|
||||
<i class="bi bi-person-circle me-2 text-primary"></i>
|
||||
<div>
|
||||
<small class="text-muted d-block">{ "Seller" }</small>
|
||||
<a href="#" class="fw-semibold text-decoration-none small">{ &listing.seller_name }</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<div class="d-flex align-items-center mb-2">
|
||||
<i class="bi bi-calendar me-2 text-primary"></i>
|
||||
<div>
|
||||
<small class="text-muted d-block">{ "Listed" }</small>
|
||||
<span class="fw-semibold small">{ &listing.created_at }</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<div class="d-flex align-items-center mb-2">
|
||||
<i class="bi bi-clock me-2 text-primary"></i>
|
||||
<div>
|
||||
<small class="text-muted d-block">{ "Expires" }</small>
|
||||
<span class="fw-semibold small">{ &listing.expires_at }</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<div class="d-flex align-items-center mb-2">
|
||||
<i class="bi bi-tag me-2 text-primary"></i>
|
||||
<div>
|
||||
<small class="text-muted d-block">{ "Category" }</small>
|
||||
<span class="fw-semibold small">{ &listing.asset_type }</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-3 pt-2 border-top">
|
||||
<h6 class="fw-semibold mb-2 small">{ "Tags" }</h6>
|
||||
<div class="d-flex flex-wrap gap-1">
|
||||
{ for listing.tags.iter().map(|tag| html! {
|
||||
<span class="badge bg-primary-subtle text-primary px-2 py-1 small">{ tag }</span>
|
||||
}) }
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
// Bids Section
|
||||
<div class="card border-0 shadow-lg">
|
||||
<div class="card-header bg-transparent border-0 p-3 pb-0">
|
||||
<h6 class="fw-bold mb-0">
|
||||
<i class="bi bi-list-ol me-2 text-primary"></i>{ "Bidding History" }
|
||||
</h6>
|
||||
</div>
|
||||
<div class="card-body p-3 pt-2" style="max-height: 200px; overflow-y: auto;">
|
||||
{ if listing.bids.is_empty() {
|
||||
html! {
|
||||
<div class="text-center py-3">
|
||||
<i class="bi bi-hammer text-muted fs-4"></i>
|
||||
<p class="text-muted mb-0 mt-2 small">{ "No bids yet. Be the first!" }</p>
|
||||
</div>
|
||||
}
|
||||
} else {
|
||||
html! {
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm table-hover mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="border-0 small">{ "Bidder" }</th>
|
||||
<th class="border-0 small">{ "Amount" }</th>
|
||||
<th class="border-0 small">{ "Time" }</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{ for listing.bids.iter().enumerate().map(|(i, bid)| html! {
|
||||
<tr class={if i == 0 { "table-success" } else { "" }}>
|
||||
<td class="fw-semibold small">
|
||||
{ if i == 0 {
|
||||
html! { <><i class="bi bi-trophy-fill text-warning me-1"></i>{ &bid.bidder_name }</> }
|
||||
} else {
|
||||
html! { <span>{ &bid.bidder_name }</span> }
|
||||
}}
|
||||
</td>
|
||||
<td class="fw-bold text-success small">{ format!("${:.2}", bid.amount) }</td>
|
||||
<td class="text-muted small">{ &bid.created_at }</td>
|
||||
</tr>
|
||||
}) }
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
}
|
||||
}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
27
marketplace/src/views/marketplace.rs
Normal file
27
marketplace/src/views/marketplace.rs
Normal file
@@ -0,0 +1,27 @@
|
||||
use yew::prelude::*;
|
||||
|
||||
#[derive(Properties, PartialEq, Clone)]
|
||||
pub struct MarketplaceViewProps {
|
||||
#[prop_or_default]
|
||||
pub asset_id: Option<String>,
|
||||
#[prop_or_default]
|
||||
pub tab: Option<String>,
|
||||
}
|
||||
|
||||
#[function_component(MarketplaceView)]
|
||||
pub fn marketplace_view(props: &MarketplaceViewProps) -> Html {
|
||||
if let Some(id) = &props.asset_id {
|
||||
return html! { <div>{ format!("Viewing asset detail for ID: {}", id) }</div> };
|
||||
}
|
||||
|
||||
if let Some(tab) = &props.tab {
|
||||
return html! { <div>{ format!("Viewing tab: {}", tab) }</div> };
|
||||
}
|
||||
|
||||
html! {
|
||||
<div>
|
||||
<h1>{ "Browse Marketplace" }</h1>
|
||||
<p>{ "Here you can find all the digital assets." }</p>
|
||||
</div>
|
||||
}
|
||||
}
|
13
marketplace/src/views/mod.rs
Normal file
13
marketplace/src/views/mod.rs
Normal file
@@ -0,0 +1,13 @@
|
||||
pub mod home;
|
||||
pub mod marketplace;
|
||||
pub mod create_listing;
|
||||
pub mod edit_listing;
|
||||
pub mod my_listings;
|
||||
pub mod listing_detail;
|
||||
|
||||
pub use home::HomeView;
|
||||
|
||||
pub use create_listing::CreateListingView;
|
||||
pub use edit_listing::EditListingView;
|
||||
pub use my_listings::MyListingsView;
|
||||
pub use listing_detail::ListingDetailView;
|
237
marketplace/src/views/my_listings.rs
Normal file
237
marketplace/src/views/my_listings.rs
Normal file
@@ -0,0 +1,237 @@
|
||||
use yew::prelude::*;
|
||||
|
||||
|
||||
#[derive(Clone, PartialEq)]
|
||||
struct Listing {
|
||||
id: String,
|
||||
asset_name: String,
|
||||
title: String,
|
||||
price: f64,
|
||||
listing_type: String,
|
||||
status: String,
|
||||
created_at: String,
|
||||
expires_at: String,
|
||||
}
|
||||
|
||||
#[function_component(MyListingsView)]
|
||||
pub fn my_listings_view() -> Html {
|
||||
let listings = vec![
|
||||
Listing {
|
||||
id: "1".to_string(),
|
||||
asset_name: "Kilifi Permaculture Farm".to_string(),
|
||||
title: "Permaculture Farm Token - Kilifi Coast".to_string(),
|
||||
price: 12500.00,
|
||||
listing_type: "Auction".to_string(),
|
||||
status: "Active".to_string(),
|
||||
created_at: "2025-06-26".to_string(),
|
||||
expires_at: "2025-07-26".to_string(),
|
||||
},
|
||||
Listing {
|
||||
id: "2".to_string(),
|
||||
asset_name: "Solar Cooperative Share".to_string(),
|
||||
title: "Community Solar Cooperative Share".to_string(),
|
||||
price: 850.00,
|
||||
listing_type: "Sale".to_string(),
|
||||
status: "Sold".to_string(),
|
||||
created_at: "2025-06-15".to_string(),
|
||||
expires_at: "N/A".to_string(),
|
||||
},
|
||||
Listing {
|
||||
id: "3".to_string(),
|
||||
asset_name: "Mangrove Biodiversity Credits".to_string(),
|
||||
title: "Biodiversity Credits - Coastal Mangroves".to_string(),
|
||||
price: 450.00,
|
||||
listing_type: "Sale".to_string(),
|
||||
status: "Expired".to_string(),
|
||||
created_at: "2025-05-20".to_string(),
|
||||
expires_at: "2025-06-20".to_string(),
|
||||
},
|
||||
];
|
||||
|
||||
let render_status_badge = |status: &str| {
|
||||
let badge_class = match status {
|
||||
"Active" => "bg-success",
|
||||
"Sold" => "bg-info",
|
||||
"Expired" => "bg-warning text-dark",
|
||||
"Cancelled" => "bg-danger",
|
||||
_ => "bg-secondary",
|
||||
};
|
||||
html! { <span class={classes!("badge", badge_class)}>{ status }</span> }
|
||||
};
|
||||
|
||||
html! {
|
||||
<div class="fade-in">
|
||||
<div class="container-fluid px-4 py-4">
|
||||
<nav aria-label="breadcrumb" class="mb-4">
|
||||
<ol class="breadcrumb">
|
||||
<li class="breadcrumb-item"><a href="/">{ "Home" }</a></li>
|
||||
<li class="breadcrumb-item"><a href="/browse">{ "Marketplace" }</a></li>
|
||||
<li class="breadcrumb-item active" aria-current="page">{ "My Listings" }</li>
|
||||
</ol>
|
||||
</nav>
|
||||
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<div>
|
||||
<h1 class="h2 fw-bold mb-2">{ "My Listings" }</h1>
|
||||
<p class="text-muted mb-0">{ "Manage your digital asset listings" }</p>
|
||||
</div>
|
||||
<a href="/create-listing" class="btn btn-marketplace">
|
||||
<i class="bi bi-plus-circle me-2"></i>{ "Create New Listing" }
|
||||
</a>
|
||||
</div>
|
||||
|
||||
// Stats Cards
|
||||
<div class="row g-4 mb-4">
|
||||
<div class="col-md-3">
|
||||
<div class="card border-0 shadow-sm">
|
||||
<div class="card-body text-center p-4">
|
||||
<i class="bi bi-list-ul text-primary fs-1 mb-3"></i>
|
||||
<h3 class="fw-bold mb-1">{ listings.len() }</h3>
|
||||
<p class="text-muted mb-0">{ "Total Listings" }</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="card border-0 shadow-sm">
|
||||
<div class="card-body text-center p-4">
|
||||
<i class="bi bi-check-circle text-success fs-1 mb-3"></i>
|
||||
<h3 class="fw-bold mb-1">{ listings.iter().filter(|l| l.status == "Active").count() }</h3>
|
||||
<p class="text-muted mb-0">{ "Active" }</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="card border-0 shadow-sm">
|
||||
<div class="card-body text-center p-4">
|
||||
<i class="bi bi-bag-check text-info fs-1 mb-3"></i>
|
||||
<h3 class="fw-bold mb-1">{ listings.iter().filter(|l| l.status == "Sold").count() }</h3>
|
||||
<p class="text-muted mb-0">{ "Sold" }</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="card border-0 shadow-sm">
|
||||
<div class="card-body text-center p-4">
|
||||
<i class="bi bi-currency-dollar text-warning fs-1 mb-3"></i>
|
||||
<h3 class="fw-bold mb-1">{ format!("${:.0}", listings.iter().filter(|l| l.status == "Sold").map(|l| l.price).sum::<f64>()) }</h3>
|
||||
<p class="text-muted mb-0">{ "Total Earned" }</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card border-0 shadow-lg">
|
||||
<div class="card-header bg-transparent border-0 p-4">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<h5 class="fw-bold mb-0">
|
||||
<i class="bi bi-list-ul me-2 text-primary"></i>
|
||||
{ "All Listings" }
|
||||
</h5>
|
||||
<div class="d-flex gap-2">
|
||||
<select class="form-select form-select-sm" style="width: auto;">
|
||||
<option>{ "All Status" }</option>
|
||||
<option>{ "Active" }</option>
|
||||
<option>{ "Sold" }</option>
|
||||
<option>{ "Expired" }</option>
|
||||
</select>
|
||||
<select class="form-select form-select-sm" style="width: auto;">
|
||||
<option>{ "All Types" }</option>
|
||||
<option>{ "Sale" }</option>
|
||||
<option>{ "Auction" }</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
{ if listings.is_empty() {
|
||||
html! {
|
||||
<div class="empty-state">
|
||||
<i class="bi bi-plus-circle"></i>
|
||||
<h3>{ "No listings yet" }</h3>
|
||||
<p>{ "Create your first listing to start selling digital assets" }</p>
|
||||
<a href="/create-listing" class="btn btn-marketplace">
|
||||
<i class="bi bi-plus-circle me-2"></i>{ "Create First Listing" }
|
||||
</a>
|
||||
</div>
|
||||
}
|
||||
} else {
|
||||
html! {
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="border-0 ps-4">{ "Asset" }</th>
|
||||
<th class="border-0">{ "Title" }</th>
|
||||
<th class="border-0">{ "Price" }</th>
|
||||
<th class="border-0">{ "Type" }</th>
|
||||
<th class="border-0">{ "Status" }</th>
|
||||
<th class="border-0">{ "Created" }</th>
|
||||
<th class="border-0">{ "Expires" }</th>
|
||||
<th class="border-0 pe-4">{ "Actions" }</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{ for listings.iter().map(|listing| html! {
|
||||
<tr>
|
||||
<td class="ps-4">
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="rounded bg-primary-subtle d-flex align-items-center justify-content-center me-3" style="width: 40px; height: 40px;">
|
||||
<i class="bi bi-image text-primary"></i>
|
||||
</div>
|
||||
<span class="fw-semibold">{ &listing.asset_name }</span>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<a href={format!("/asset/{}", listing.id)} class="text-decoration-none fw-semibold">
|
||||
{ &listing.title }
|
||||
</a>
|
||||
</td>
|
||||
<td class="fw-bold">{ format!("${:.2}", listing.price) }</td>
|
||||
<td>
|
||||
<span class="badge bg-primary-subtle text-primary px-3 py-2">
|
||||
{ &listing.listing_type }
|
||||
</span>
|
||||
</td>
|
||||
<td>{ render_status_badge(&listing.status) }</td>
|
||||
<td class="text-muted">{ &listing.created_at }</td>
|
||||
<td class="text-muted">{ &listing.expires_at }</td>
|
||||
<td class="pe-4">
|
||||
<div class="d-flex gap-2">
|
||||
<a href={format!("/asset/{}", listing.id)}
|
||||
class="btn btn-sm btn-outline-primary"
|
||||
title="View Details">
|
||||
<i class="bi bi-eye"></i>
|
||||
</a>
|
||||
{ if listing.status == "Active" {
|
||||
html! {
|
||||
<>
|
||||
<button type="button"
|
||||
class="btn btn-sm btn-outline-secondary"
|
||||
title="Edit Listing">
|
||||
<i class="bi bi-pencil"></i>
|
||||
</button>
|
||||
<button type="button"
|
||||
class="btn btn-sm btn-outline-danger"
|
||||
title="Cancel Listing">
|
||||
<i class="bi bi-x-circle"></i>
|
||||
</button>
|
||||
</>
|
||||
}
|
||||
} else {
|
||||
html! {}
|
||||
}}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
}) }
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
}
|
||||
}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
466
marketplace/static/css/main.css
Normal file
466
marketplace/static/css/main.css
Normal file
@@ -0,0 +1,466 @@
|
||||
/* Marketplace App Custom Styles */
|
||||
|
||||
/* Layout */
|
||||
.main-content {
|
||||
min-height: calc(100vh - 120px);
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
width: 250px;
|
||||
min-height: calc(100vh - 56px);
|
||||
transition: margin-left 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
.sidebar.show {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.sidebar {
|
||||
position: fixed;
|
||||
top: 56px;
|
||||
left: 0;
|
||||
z-index: 1000;
|
||||
margin-left: -250px;
|
||||
background-color: white !important;
|
||||
box-shadow: 2px 0 5px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.sidebar.show {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.main-content {
|
||||
margin-left: 0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* Hero Section */
|
||||
.hero-section {
|
||||
background: linear-gradient(135deg, var(--primary-color) 0%, var(--secondary-color) 100%);
|
||||
color: white !important;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
min-height: 60vh;
|
||||
}
|
||||
|
||||
.hero-section::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><defs><pattern id="grain" width="100" height="100" patternUnits="userSpaceOnUse"><circle cx="50" cy="50" r="1" fill="white" opacity="0.1"/></pattern></defs><rect width="100" height="100" fill="url(%23grain)"/></svg>');
|
||||
opacity: 0.1;
|
||||
}
|
||||
|
||||
.hero-section .container-fluid {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.hero-section .text-warning {
|
||||
color: #ffc107 !important;
|
||||
}
|
||||
|
||||
/* Dark theme hero text */
|
||||
[data-bs-theme="dark"] .hero-section .hero-title,
|
||||
[data-bs-theme="dark"] .hero-section .hero-subtitle {
|
||||
color: white !important;
|
||||
}
|
||||
|
||||
/* Marketplace specific styles */
|
||||
.marketplace-card {
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
background: var(--bg-primary);
|
||||
border: 1px solid var(--border-light);
|
||||
box-shadow: var(--shadow-sm);
|
||||
}
|
||||
|
||||
.marketplace-card:hover {
|
||||
transform: translateY(-8px) scale(1.02);
|
||||
box-shadow: var(--shadow-xl);
|
||||
border-color: var(--primary-color);
|
||||
}
|
||||
|
||||
.marketplace-card .card-img-top {
|
||||
height: 250px;
|
||||
object-fit: cover;
|
||||
transition: transform 0.4s ease;
|
||||
}
|
||||
|
||||
.marketplace-card:hover .card-img-top {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
.marketplace-card .card-body {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.marketplace-card .card-footer {
|
||||
background: var(--bg-tertiary);
|
||||
border-top: 1px solid var(--border-light);
|
||||
padding: 1rem 1.5rem;
|
||||
}
|
||||
|
||||
.asset-image {
|
||||
height: 250px;
|
||||
object-fit: cover;
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.marketplace-card:hover .asset-image {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
/* Price and Badge Styling */
|
||||
.price-badge {
|
||||
background-color: var(--primary-color);
|
||||
color: white;
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 6px;
|
||||
font-weight: 700;
|
||||
font-size: 1.1rem;
|
||||
box-shadow: var(--shadow-sm);
|
||||
}
|
||||
|
||||
.auction-badge {
|
||||
background: linear-gradient(135deg, #ec4899 0%, #f59e0b 100%);
|
||||
color: white;
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 20px;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
animation: pulse-glow 2s infinite;
|
||||
box-shadow: 0 0 20px rgba(236, 72, 153, 0.3);
|
||||
}
|
||||
|
||||
@keyframes pulse-glow {
|
||||
0%, 100% {
|
||||
opacity: 1;
|
||||
box-shadow: 0 0 20px rgba(236, 72, 153, 0.3);
|
||||
}
|
||||
50% {
|
||||
opacity: 0.8;
|
||||
box-shadow: 0 0 30px rgba(236, 72, 153, 0.5);
|
||||
}
|
||||
}
|
||||
|
||||
.category-filter {
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
backdrop-filter: blur(10px);
|
||||
border-radius: 15px;
|
||||
padding: 1rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
/* Filter Bar */
|
||||
.filter-bar {
|
||||
background: var(--bg-primary);
|
||||
border-bottom: 1px solid var(--border-light);
|
||||
box-shadow: var(--shadow-sm);
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.filter-bar.sticky-top {
|
||||
z-index: 1020;
|
||||
}
|
||||
|
||||
/* Search Container */
|
||||
.search-container {
|
||||
background: var(--bg-primary);
|
||||
backdrop-filter: blur(10px);
|
||||
border-radius: 8px;
|
||||
padding: 2rem;
|
||||
margin-bottom: 2rem;
|
||||
box-shadow: var(--shadow-md);
|
||||
border: 1px solid var(--border-light);
|
||||
}
|
||||
|
||||
.asset-detail-image {
|
||||
border-radius: 15px;
|
||||
box-shadow: 0 10px 30px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.breadcrumb {
|
||||
background: transparent;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.breadcrumb-item + .breadcrumb-item::before {
|
||||
content: "›";
|
||||
font-size: 1.2rem;
|
||||
color: #6c757d;
|
||||
}
|
||||
|
||||
/* Button styles */
|
||||
.btn-marketplace {
|
||||
background-color: var(--primary-color);
|
||||
border: 1px solid var(--primary-color);
|
||||
color: white;
|
||||
padding: 0.75rem 1.5rem;
|
||||
border-radius: 6px;
|
||||
font-weight: 600;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.btn-marketplace:hover {
|
||||
background-color: var(--primary-hover);
|
||||
border-color: var(--primary-hover);
|
||||
transform: translateY(-1px);
|
||||
box-shadow: var(--shadow-md);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-bid {
|
||||
background-color: #ec4899;
|
||||
border: 1px solid #ec4899;
|
||||
color: white;
|
||||
padding: 0.75rem 1.5rem;
|
||||
border-radius: 6px;
|
||||
font-weight: 600;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.btn-bid:hover {
|
||||
background-color: #db2777;
|
||||
border-color: #db2777;
|
||||
transform: translateY(-1px);
|
||||
box-shadow: var(--shadow-md);
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* Search and filters */
|
||||
.search-container {
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
backdrop-filter: blur(10px);
|
||||
border-radius: 15px;
|
||||
padding: 1.5rem;
|
||||
margin-bottom: 2rem;
|
||||
box-shadow: 0 5px 20px rgba(0,0,0,0.05);
|
||||
}
|
||||
|
||||
.form-control:focus, .form-select:focus {
|
||||
border-color: #0099FF;
|
||||
box-shadow: 0 0 0 0.2rem rgba(102, 126, 234, 0.25);
|
||||
}
|
||||
|
||||
/* Empty state */
|
||||
.empty-state {
|
||||
padding: 4rem 2rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.empty-state i {
|
||||
font-size: 4rem;
|
||||
color: #dee2e6;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
/* Responsive adjustments */
|
||||
@media (max-width: 576px) {
|
||||
.marketplace-card {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.search-container {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.asset-detail-image {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* Loading States */
|
||||
.loading-skeleton {
|
||||
background: linear-gradient(90deg, var(--bg-tertiary) 25%, var(--border-light) 50%, var(--bg-tertiary) 75%);
|
||||
background-size: 200% 100%;
|
||||
animation: loading 1.5s infinite;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
@keyframes loading {
|
||||
0% { background-position: 200% 0; }
|
||||
100% { background-position: -200% 0; }
|
||||
}
|
||||
|
||||
/* Custom Scrollbar */
|
||||
::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: var(--bg-tertiary);
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: var(--gradient-primary);
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--primary-hover);
|
||||
}
|
||||
|
||||
/* Responsive Design */
|
||||
@media (max-width: 576px) {
|
||||
.marketplace-card {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.search-container {
|
||||
padding: 1rem;
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.asset-detail-image {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.hero-section {
|
||||
padding: 2rem 0 !important;
|
||||
}
|
||||
|
||||
.hero-section h1 {
|
||||
font-size: 2rem !important;
|
||||
}
|
||||
|
||||
.btn-marketplace,
|
||||
.btn-bid {
|
||||
padding: 0.75rem 1.5rem;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.price-badge {
|
||||
padding: 0.5rem 1rem;
|
||||
font-size: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.filter-bar .row > div {
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.filter-bar .row > div:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* Animation Utilities */
|
||||
.fade-in {
|
||||
animation: fadeIn 0.6s ease-in-out;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; transform: translateY(20px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
.slide-up {
|
||||
animation: slideUp 0.4s ease-out;
|
||||
}
|
||||
|
||||
@keyframes slideUp {
|
||||
from { transform: translateY(30px); opacity: 0; }
|
||||
to { transform: translateY(0); opacity: 1; }
|
||||
}
|
||||
|
||||
/* Utility Classes */
|
||||
.text-gradient {
|
||||
background: var(--gradient-primary);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.border-gradient {
|
||||
border: 2px solid transparent;
|
||||
background: linear-gradient(var(--bg-primary), var(--bg-primary)) padding-box,
|
||||
var(--gradient-primary) border-box;
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.glass-effect {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
backdrop-filter: blur(10px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
/* Dark theme adjustments */
|
||||
[data-bs-theme="dark"] .hero-section {
|
||||
background: linear-gradient(135deg, #1e293b 0%, #334155 100%) !important;
|
||||
}
|
||||
|
||||
[data-bs-theme="dark"] .marketplace-card {
|
||||
background: var(--bg-primary) !important;
|
||||
border-color: var(--border-light) !important;
|
||||
color: var(--text-primary) !important;
|
||||
}
|
||||
|
||||
[data-bs-theme="dark"] .marketplace-card .card-body {
|
||||
color: var(--text-primary) !important;
|
||||
}
|
||||
|
||||
[data-bs-theme="dark"] .marketplace-card .text-muted {
|
||||
color: var(--text-muted) !important;
|
||||
}
|
||||
|
||||
[data-bs-theme="dark"] .filter-bar {
|
||||
background: var(--bg-primary) !important;
|
||||
border-bottom-color: var(--border-light) !important;
|
||||
}
|
||||
|
||||
[data-bs-theme="dark"] .search-container {
|
||||
background: var(--bg-primary) !important;
|
||||
border-color: var(--border-light) !important;
|
||||
}
|
||||
|
||||
[data-bs-theme="dark"] .table {
|
||||
color: var(--text-primary) !important;
|
||||
}
|
||||
|
||||
[data-bs-theme="dark"] .table thead th {
|
||||
background: var(--bg-tertiary) !important;
|
||||
color: var(--text-primary) !important;
|
||||
border-color: var(--border-light) !important;
|
||||
}
|
||||
|
||||
[data-bs-theme="dark"] .table tbody td {
|
||||
color: var(--text-primary) !important;
|
||||
border-color: var(--border-light) !important;
|
||||
}
|
||||
|
||||
[data-bs-theme="dark"] .table tbody tr:hover {
|
||||
background-color: var(--bg-tertiary) !important;
|
||||
}
|
||||
|
||||
[data-bs-theme="dark"] .empty-state {
|
||||
background: var(--bg-primary) !important;
|
||||
border-color: var(--border-medium) !important;
|
||||
color: var(--text-primary) !important;
|
||||
}
|
||||
|
||||
[data-bs-theme="dark"] .empty-state h3,
|
||||
[data-bs-theme="dark"] .empty-state h6 {
|
||||
color: var(--text-secondary) !important;
|
||||
}
|
||||
|
||||
[data-bs-theme="dark"] .empty-state p {
|
||||
color: var(--text-muted) !important;
|
||||
}
|
||||
|
||||
[data-bs-theme="dark"] .glass-effect {
|
||||
background: rgba(0, 0, 0, 0.2) !important;
|
||||
border-color: rgba(255, 255, 255, 0.1) !important;
|
||||
}
|
||||
|
||||
[data-bs-theme="dark"] .bg-light {
|
||||
background-color: var(--bg-tertiary) !important;
|
||||
}
|
301
marketplace/static/style.css
Normal file
301
marketplace/static/style.css
Normal file
@@ -0,0 +1,301 @@
|
||||
:root {
|
||||
/* Modern marketplace color palette */
|
||||
--primary-color: #6366f1;
|
||||
--primary-hover: #5b5bd6;
|
||||
--secondary-color: #f59e0b;
|
||||
--success-color: #10b981;
|
||||
--danger-color: #ef4444;
|
||||
--warning-color: #f59e0b;
|
||||
--info-color: #3b82f6;
|
||||
|
||||
/* Background colors */
|
||||
--bg-primary: #ffffff;
|
||||
--bg-secondary: #f8fafc;
|
||||
--bg-tertiary: #f1f5f9;
|
||||
--bg-dark: #1e293b;
|
||||
--bg-darker: #0f172a;
|
||||
|
||||
/* Text colors */
|
||||
--text-primary: #1e293b;
|
||||
--text-secondary: #64748b;
|
||||
--text-muted: #94a3b8;
|
||||
--text-light: #ffffff;
|
||||
|
||||
/* Border colors */
|
||||
--border-light: #e2e8f0;
|
||||
--border-medium: #cbd5e1;
|
||||
--border-dark: #475569;
|
||||
|
||||
/* Shadows */
|
||||
--shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.05);
|
||||
--shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
|
||||
--shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
|
||||
--shadow-xl: 0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1);
|
||||
|
||||
/* Gradients */
|
||||
--gradient-primary: linear-gradient(135deg, var(--primary-color) 0%, var(--secondary-color) 100%);
|
||||
--gradient-success: linear-gradient(135deg, var(--success-color) 0%, #059669 100%);
|
||||
--gradient-info: linear-gradient(135deg, var(--info-color) 0%, #2563eb 100%);
|
||||
}
|
||||
|
||||
/* Light theme (default) */
|
||||
body {
|
||||
background-color: var(--bg-secondary);
|
||||
color: var(--text-primary);
|
||||
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
/* Card styling */
|
||||
.card {
|
||||
background-color: var(--bg-primary);
|
||||
border: 1px solid var(--border-light);
|
||||
border-radius: 12px;
|
||||
box-shadow: var(--shadow-sm);
|
||||
transition: all 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
.card:hover {
|
||||
box-shadow: var(--shadow-md);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.card-header {
|
||||
background-color: var(--bg-tertiary);
|
||||
border-bottom: 1px solid var(--border-light);
|
||||
border-radius: 12px 12px 0 0 !important;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
/* Navigation styling */
|
||||
.navbar {
|
||||
background: var(--bg-primary) !important;
|
||||
border-bottom: 1px solid var(--border-light);
|
||||
box-shadow: var(--shadow-sm);
|
||||
}
|
||||
|
||||
.navbar-brand {
|
||||
font-weight: 700;
|
||||
color: var(--primary-color) !important;
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.nav-link {
|
||||
color: var(--text-secondary) !important;
|
||||
font-weight: 500;
|
||||
transition: color 0.2s ease;
|
||||
}
|
||||
|
||||
.nav-link:hover {
|
||||
color: var(--primary-color) !important;
|
||||
}
|
||||
|
||||
.nav-link.active {
|
||||
color: var(--primary-color) !important;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* Sidebar styling */
|
||||
.sidebar {
|
||||
background-color: var(--bg-primary) !important;
|
||||
border-right: 1px solid var(--border-light);
|
||||
box-shadow: var(--shadow-sm);
|
||||
}
|
||||
|
||||
/* Button styling */
|
||||
.btn-primary {
|
||||
background-color: var(--primary-color);
|
||||
border: 1px solid var(--primary-color);
|
||||
border-radius: 6px;
|
||||
font-weight: 600;
|
||||
padding: 0.75rem 1.5rem;
|
||||
transition: all 0.2s ease;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background-color: var(--primary-hover);
|
||||
border-color: var(--primary-hover);
|
||||
transform: translateY(-1px);
|
||||
box-shadow: var(--shadow-md);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background-color: var(--bg-tertiary);
|
||||
border: 1px solid var(--border-medium);
|
||||
color: var(--text-primary);
|
||||
border-radius: 6px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background-color: var(--border-light);
|
||||
border-color: var(--border-dark);
|
||||
}
|
||||
|
||||
/* Form styling */
|
||||
.form-control, .form-select {
|
||||
border: 1px solid var(--border-medium);
|
||||
border-radius: 8px;
|
||||
padding: 0.75rem 1rem;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.form-control:focus, .form-select:focus {
|
||||
border-color: var(--primary-color);
|
||||
box-shadow: 0 0 0 3px rgb(99 102 241 / 0.1);
|
||||
}
|
||||
|
||||
/* Badge styling */
|
||||
.badge {
|
||||
border-radius: 6px;
|
||||
font-weight: 500;
|
||||
padding: 0.5rem 0.75rem;
|
||||
}
|
||||
|
||||
.bg-success {
|
||||
background: var(--gradient-success) !important;
|
||||
}
|
||||
|
||||
.bg-info {
|
||||
background: var(--gradient-info) !important;
|
||||
}
|
||||
|
||||
.bg-warning {
|
||||
background-color: var(--warning-color) !important;
|
||||
}
|
||||
|
||||
.bg-danger {
|
||||
background-color: var(--danger-color) !important;
|
||||
}
|
||||
|
||||
/* Dark theme support */
|
||||
[data-bs-theme="dark"] {
|
||||
--bg-primary: #1e293b;
|
||||
--bg-secondary: #0f172a;
|
||||
--bg-tertiary: #334155;
|
||||
--text-primary: #f8fafc;
|
||||
--text-secondary: #cbd5e1;
|
||||
--text-muted: #94a3b8;
|
||||
--border-light: #475569;
|
||||
--border-medium: #64748b;
|
||||
}
|
||||
|
||||
[data-bs-theme="dark"] body {
|
||||
background-color: var(--bg-secondary) !important;
|
||||
color: var(--text-primary) !important;
|
||||
}
|
||||
|
||||
[data-bs-theme="dark"] .navbar {
|
||||
background: var(--bg-primary) !important;
|
||||
border-bottom-color: var(--border-light);
|
||||
}
|
||||
|
||||
[data-bs-theme="dark"] .card {
|
||||
background-color: var(--bg-primary) !important;
|
||||
border-color: var(--border-light) !important;
|
||||
color: var(--text-primary) !important;
|
||||
}
|
||||
|
||||
[data-bs-theme="dark"] .card-body,
|
||||
[data-bs-theme="dark"] .card-header,
|
||||
[data-bs-theme="dark"] .card-footer {
|
||||
background-color: transparent !important;
|
||||
color: var(--text-primary) !important;
|
||||
}
|
||||
|
||||
[data-bs-theme="dark"] .card-header {
|
||||
background-color: var(--bg-tertiary) !important;
|
||||
border-bottom-color: var(--border-light) !important;
|
||||
}
|
||||
|
||||
[data-bs-theme="dark"] .text-muted {
|
||||
color: var(--text-muted) !important;
|
||||
}
|
||||
|
||||
[data-bs-theme="dark"] .sidebar {
|
||||
background-color: var(--bg-primary) !important;
|
||||
border-right-color: var(--border-light);
|
||||
}
|
||||
|
||||
[data-bs-theme="dark"] .form-control,
|
||||
[data-bs-theme="dark"] .form-select {
|
||||
background-color: var(--bg-tertiary) !important;
|
||||
border-color: var(--border-medium) !important;
|
||||
color: var(--text-primary) !important;
|
||||
}
|
||||
|
||||
[data-bs-theme="dark"] .form-control:focus,
|
||||
[data-bs-theme="dark"] .form-select:focus {
|
||||
background-color: var(--bg-tertiary) !important;
|
||||
border-color: var(--primary-color) !important;
|
||||
color: var(--text-primary) !important;
|
||||
}
|
||||
|
||||
[data-bs-theme="dark"] .btn-secondary {
|
||||
background-color: var(--bg-tertiary) !important;
|
||||
border-color: var(--border-medium) !important;
|
||||
color: var(--text-primary) !important;
|
||||
}
|
||||
|
||||
[data-bs-theme="dark"] .btn-outline-secondary {
|
||||
border-color: var(--border-medium) !important;
|
||||
color: var(--text-secondary) !important;
|
||||
}
|
||||
|
||||
[data-bs-theme="dark"] .btn-outline-secondary:hover {
|
||||
background-color: var(--bg-tertiary) !important;
|
||||
border-color: var(--border-light) !important;
|
||||
color: var(--text-primary) !important;
|
||||
}
|
||||
|
||||
[data-bs-theme="dark"] .bg-light {
|
||||
background-color: var(--bg-tertiary) !important;
|
||||
}
|
||||
|
||||
[data-bs-theme="dark"] .border-top {
|
||||
border-color: var(--border-light) !important;
|
||||
}
|
||||
|
||||
[data-bs-theme="dark"] .dropdown-menu {
|
||||
background-color: var(--bg-primary) !important;
|
||||
border-color: var(--border-light) !important;
|
||||
}
|
||||
|
||||
[data-bs-theme="dark"] .dropdown-item {
|
||||
color: var(--text-primary) !important;
|
||||
}
|
||||
|
||||
[data-bs-theme="dark"] .dropdown-item:hover {
|
||||
background-color: var(--bg-tertiary) !important;
|
||||
color: var(--text-primary) !important;
|
||||
}
|
||||
|
||||
[data-bs-theme="dark"] .breadcrumb-item a {
|
||||
color: var(--primary-color) !important;
|
||||
}
|
||||
|
||||
[data-bs-theme="dark"] .breadcrumb-item.active {
|
||||
color: var(--text-muted) !important;
|
||||
}
|
||||
|
||||
[data-bs-theme="dark"] .table {
|
||||
color: var(--text-primary) !important;
|
||||
}
|
||||
|
||||
[data-bs-theme="dark"] .table thead th {
|
||||
background-color: var(--bg-tertiary) !important;
|
||||
border-color: var(--border-light) !important;
|
||||
color: var(--text-primary) !important;
|
||||
}
|
||||
|
||||
[data-bs-theme="dark"] .table tbody td {
|
||||
border-color: var(--border-light) !important;
|
||||
color: var(--text-primary) !important;
|
||||
}
|
||||
|
||||
[data-bs-theme="dark"] .table tbody tr:hover {
|
||||
background-color: var(--bg-tertiary) !important;
|
||||
}
|
Reference in New Issue
Block a user