mirror of
https://github.com/aljazceru/cdk.git
synced 2025-12-18 21:25:09 +01:00
Improve web interface with dynamic status, navigation, and mobile support (#1073)
* Improve transaction confirmation UI: reorder elements, move buttons to details card, shorten button text * feat: real node status --------- Co-authored-by: thesimplekid <tsk@thesimplekid.com>
This commit is contained in:
@@ -15,7 +15,8 @@ use serde::Deserialize;
|
|||||||
use crate::web::handlers::utils::deserialize_optional_u64;
|
use crate::web::handlers::utils::deserialize_optional_u64;
|
||||||
use crate::web::handlers::AppState;
|
use crate::web::handlers::AppState;
|
||||||
use crate::web::templates::{
|
use crate::web::templates::{
|
||||||
error_message, form_card, format_sats_as_btc, info_card, layout, success_message,
|
error_message, form_card, format_sats_as_btc, info_card, is_node_running, layout_with_status,
|
||||||
|
success_message,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
@@ -43,7 +44,7 @@ pub async fn channels_page(State(_state): State<AppState>) -> Result<Response, S
|
|||||||
.unwrap())
|
.unwrap())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn open_channel_page(State(_state): State<AppState>) -> Result<Html<String>, StatusCode> {
|
pub async fn open_channel_page(State(state): State<AppState>) -> Result<Html<String>, StatusCode> {
|
||||||
let content = form_card(
|
let content = form_card(
|
||||||
"Open New Channel",
|
"Open New Channel",
|
||||||
html! {
|
html! {
|
||||||
@@ -68,14 +69,18 @@ pub async fn open_channel_page(State(_state): State<AppState>) -> Result<Html<St
|
|||||||
label for="push_btc" { "Push Amount (optional)" }
|
label for="push_btc" { "Push Amount (optional)" }
|
||||||
input type="number" id="push_btc" name="push_btc" placeholder="₿0" step="1" {}
|
input type="number" id="push_btc" name="push_btc" placeholder="₿0" step="1" {}
|
||||||
}
|
}
|
||||||
button type="submit" { "Open Channel" }
|
div class="form-actions" {
|
||||||
" "
|
a href="/balance" { button type="button" class="button-secondary" { "Cancel" } }
|
||||||
a href="/balance" { button type="button" { "Cancel" } }
|
button type="submit" class="button-primary" { "Open Channel" }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
Ok(Html(layout("Open Channel", content).into_string()))
|
let is_running = is_node_running(&state.node.inner);
|
||||||
|
Ok(Html(
|
||||||
|
layout_with_status("Open Channel", content, is_running).into_string(),
|
||||||
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn post_open_channel(
|
pub async fn post_open_channel(
|
||||||
@@ -105,7 +110,7 @@ pub async fn post_open_channel(
|
|||||||
.status(StatusCode::BAD_REQUEST)
|
.status(StatusCode::BAD_REQUEST)
|
||||||
.header("content-type", "text/html")
|
.header("content-type", "text/html")
|
||||||
.body(Body::from(
|
.body(Body::from(
|
||||||
layout("Open Channel Error", content).into_string(),
|
layout_with_status("Open Channel Error", content, true).into_string(),
|
||||||
))
|
))
|
||||||
.unwrap());
|
.unwrap());
|
||||||
}
|
}
|
||||||
@@ -125,7 +130,7 @@ pub async fn post_open_channel(
|
|||||||
.status(StatusCode::BAD_REQUEST)
|
.status(StatusCode::BAD_REQUEST)
|
||||||
.header("content-type", "text/html")
|
.header("content-type", "text/html")
|
||||||
.body(Body::from(
|
.body(Body::from(
|
||||||
layout("Open Channel Error", content).into_string(),
|
layout_with_status("Open Channel Error", content, true).into_string(),
|
||||||
))
|
))
|
||||||
.unwrap());
|
.unwrap());
|
||||||
}
|
}
|
||||||
@@ -149,7 +154,7 @@ pub async fn post_open_channel(
|
|||||||
.status(StatusCode::INTERNAL_SERVER_ERROR)
|
.status(StatusCode::INTERNAL_SERVER_ERROR)
|
||||||
.header("content-type", "text/html")
|
.header("content-type", "text/html")
|
||||||
.body(Body::from(
|
.body(Body::from(
|
||||||
layout("Open Channel Error", content).into_string(),
|
layout_with_status("Open Channel Error", content, true).into_string(),
|
||||||
))
|
))
|
||||||
.unwrap());
|
.unwrap());
|
||||||
}
|
}
|
||||||
@@ -207,7 +212,7 @@ pub async fn post_open_channel(
|
|||||||
Ok(Response::builder()
|
Ok(Response::builder()
|
||||||
.header("content-type", "text/html")
|
.header("content-type", "text/html")
|
||||||
.body(Body::from(
|
.body(Body::from(
|
||||||
layout("Open Channel Result", content).into_string(),
|
layout_with_status("Open Channel Result", content, true).into_string(),
|
||||||
))
|
))
|
||||||
.unwrap())
|
.unwrap())
|
||||||
}
|
}
|
||||||
@@ -226,7 +231,9 @@ pub async fn close_channel_page(
|
|||||||
a href="/balance" { button { "← Back to Lightning" } }
|
a href="/balance" { button { "← Back to Lightning" } }
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
return Ok(Html(layout("Close Channel Error", content).into_string()));
|
return Ok(Html(
|
||||||
|
layout_with_status("Close Channel Error", content, true).into_string(),
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get channel information for amount display
|
// Get channel information for amount display
|
||||||
@@ -267,7 +274,10 @@ pub async fn close_channel_page(
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
Ok(Html(layout("Close Channel", content).into_string()))
|
let is_running = is_node_running(&state.node.inner);
|
||||||
|
Ok(Html(
|
||||||
|
layout_with_status("Close Channel", content, is_running).into_string(),
|
||||||
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn force_close_channel_page(
|
pub async fn force_close_channel_page(
|
||||||
@@ -285,7 +295,7 @@ pub async fn force_close_channel_page(
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
return Ok(Html(
|
return Ok(Html(
|
||||||
layout("Force Close Channel Error", content).into_string(),
|
layout_with_status("Force Close Channel Error", content, true).into_string(),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -335,7 +345,10 @@ pub async fn force_close_channel_page(
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
Ok(Html(layout("Force Close Channel", content).into_string()))
|
let is_running = is_node_running(&state.node.inner);
|
||||||
|
Ok(Html(
|
||||||
|
layout_with_status("Force Close Channel", content, is_running).into_string(),
|
||||||
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn post_close_channel(
|
pub async fn post_close_channel(
|
||||||
@@ -365,7 +378,7 @@ pub async fn post_close_channel(
|
|||||||
.status(StatusCode::BAD_REQUEST)
|
.status(StatusCode::BAD_REQUEST)
|
||||||
.header("content-type", "text/html")
|
.header("content-type", "text/html")
|
||||||
.body(Body::from(
|
.body(Body::from(
|
||||||
layout("Close Channel Error", content).into_string(),
|
layout_with_status("Close Channel Error", content, true).into_string(),
|
||||||
))
|
))
|
||||||
.unwrap());
|
.unwrap());
|
||||||
}
|
}
|
||||||
@@ -385,7 +398,7 @@ pub async fn post_close_channel(
|
|||||||
.status(StatusCode::BAD_REQUEST)
|
.status(StatusCode::BAD_REQUEST)
|
||||||
.header("content-type", "text/html")
|
.header("content-type", "text/html")
|
||||||
.body(Body::from(
|
.body(Body::from(
|
||||||
layout("Close Channel Error", content).into_string(),
|
layout_with_status("Close Channel Error", content, true).into_string(),
|
||||||
))
|
))
|
||||||
.unwrap());
|
.unwrap());
|
||||||
}
|
}
|
||||||
@@ -436,7 +449,7 @@ pub async fn post_close_channel(
|
|||||||
Ok(Response::builder()
|
Ok(Response::builder()
|
||||||
.header("content-type", "text/html")
|
.header("content-type", "text/html")
|
||||||
.body(Body::from(
|
.body(Body::from(
|
||||||
layout("Close Channel Result", content).into_string(),
|
layout_with_status("Close Channel Result", content, true).into_string(),
|
||||||
))
|
))
|
||||||
.unwrap())
|
.unwrap())
|
||||||
}
|
}
|
||||||
@@ -468,7 +481,7 @@ pub async fn post_force_close_channel(
|
|||||||
.status(StatusCode::BAD_REQUEST)
|
.status(StatusCode::BAD_REQUEST)
|
||||||
.header("content-type", "text/html")
|
.header("content-type", "text/html")
|
||||||
.body(Body::from(
|
.body(Body::from(
|
||||||
layout("Force Close Channel Error", content).into_string(),
|
layout_with_status("Force Close Channel Error", content, true).into_string(),
|
||||||
))
|
))
|
||||||
.unwrap());
|
.unwrap());
|
||||||
}
|
}
|
||||||
@@ -488,7 +501,7 @@ pub async fn post_force_close_channel(
|
|||||||
.status(StatusCode::BAD_REQUEST)
|
.status(StatusCode::BAD_REQUEST)
|
||||||
.header("content-type", "text/html")
|
.header("content-type", "text/html")
|
||||||
.body(Body::from(
|
.body(Body::from(
|
||||||
layout("Force Close Channel Error", content).into_string(),
|
layout_with_status("Force Close Channel Error", content, true).into_string(),
|
||||||
))
|
))
|
||||||
.unwrap());
|
.unwrap());
|
||||||
}
|
}
|
||||||
@@ -541,7 +554,7 @@ pub async fn post_force_close_channel(
|
|||||||
Ok(Response::builder()
|
Ok(Response::builder()
|
||||||
.header("content-type", "text/html")
|
.header("content-type", "text/html")
|
||||||
.body(Body::from(
|
.body(Body::from(
|
||||||
layout("Force Close Channel Result", content).into_string(),
|
layout_with_status("Force Close Channel Result", content, true).into_string(),
|
||||||
))
|
))
|
||||||
.unwrap())
|
.unwrap())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ use ldk_node::payment::{PaymentDirection, PaymentKind, PaymentStatus};
|
|||||||
use maud::html;
|
use maud::html;
|
||||||
|
|
||||||
use crate::web::handlers::AppState;
|
use crate::web::handlers::AppState;
|
||||||
use crate::web::templates::{format_sats_as_btc, layout};
|
use crate::web::templates::{format_sats_as_btc, is_node_running, layout_with_status};
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct UsageMetrics {
|
pub struct UsageMetrics {
|
||||||
@@ -272,5 +272,8 @@ pub async fn dashboard(State(state): State<AppState>) -> Result<Html<String>, St
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(Html(layout("Dashboard", content).into_string()))
|
let is_running = is_node_running(&state.node.inner);
|
||||||
|
Ok(Html(
|
||||||
|
layout_with_status("Dashboard", content, is_running).into_string(),
|
||||||
|
))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,8 @@ use serde::Deserialize;
|
|||||||
use crate::web::handlers::utils::{deserialize_optional_f64, deserialize_optional_u32};
|
use crate::web::handlers::utils::{deserialize_optional_f64, deserialize_optional_u32};
|
||||||
use crate::web::handlers::AppState;
|
use crate::web::handlers::AppState;
|
||||||
use crate::web::templates::{
|
use crate::web::templates::{
|
||||||
error_message, form_card, format_sats_as_btc, info_card, layout, success_message,
|
error_message, form_card, format_sats_as_btc, info_card, is_node_running, layout_with_status,
|
||||||
|
success_message,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
@@ -30,7 +31,7 @@ pub struct CreateBolt12Form {
|
|||||||
expiry_seconds: Option<u32>,
|
expiry_seconds: Option<u32>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn invoices_page(State(_state): State<AppState>) -> Result<Html<String>, StatusCode> {
|
pub async fn invoices_page(State(state): State<AppState>) -> Result<Html<String>, StatusCode> {
|
||||||
let content = html! {
|
let content = html! {
|
||||||
h2 style="text-align: center; margin-bottom: 3rem;" { "Invoices" }
|
h2 style="text-align: center; margin-bottom: 3rem;" { "Invoices" }
|
||||||
div class="grid" {
|
div class="grid" {
|
||||||
@@ -78,7 +79,10 @@ pub async fn invoices_page(State(_state): State<AppState>) -> Result<Html<String
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(Html(layout("Create Invoices", content).into_string()))
|
let is_running = is_node_running(&state.node.inner);
|
||||||
|
Ok(Html(
|
||||||
|
layout_with_status("Create Invoices", content, is_running).into_string(),
|
||||||
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn post_create_bolt11(
|
pub async fn post_create_bolt11(
|
||||||
@@ -122,7 +126,7 @@ pub async fn post_create_bolt11(
|
|||||||
.status(StatusCode::BAD_REQUEST)
|
.status(StatusCode::BAD_REQUEST)
|
||||||
.header("content-type", "text/html")
|
.header("content-type", "text/html")
|
||||||
.body(Body::from(
|
.body(Body::from(
|
||||||
layout("Create Invoice Error", content).into_string(),
|
layout_with_status("Create Invoice Error", content, true).into_string(),
|
||||||
))
|
))
|
||||||
.unwrap());
|
.unwrap());
|
||||||
}
|
}
|
||||||
@@ -193,7 +197,7 @@ pub async fn post_create_bolt11(
|
|||||||
Ok(Response::builder()
|
Ok(Response::builder()
|
||||||
.header("content-type", "text/html")
|
.header("content-type", "text/html")
|
||||||
.body(Body::from(
|
.body(Body::from(
|
||||||
layout("BOLT11 Invoice Created", content).into_string(),
|
layout_with_status("BOLT11 Invoice Created", content, true).into_string(),
|
||||||
))
|
))
|
||||||
.unwrap())
|
.unwrap())
|
||||||
}
|
}
|
||||||
@@ -287,7 +291,7 @@ pub async fn post_create_bolt12(
|
|||||||
Ok(Response::builder()
|
Ok(Response::builder()
|
||||||
.header("content-type", "text/html")
|
.header("content-type", "text/html")
|
||||||
.body(Body::from(
|
.body(Body::from(
|
||||||
layout("BOLT12 Offer Created", content).into_string(),
|
layout_with_status("BOLT12 Offer Created", content, true).into_string(),
|
||||||
))
|
))
|
||||||
.unwrap())
|
.unwrap())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ use axum::response::Html;
|
|||||||
use maud::html;
|
use maud::html;
|
||||||
|
|
||||||
use crate::web::handlers::utils::AppState;
|
use crate::web::handlers::utils::AppState;
|
||||||
use crate::web::templates::{format_sats_as_btc, layout};
|
use crate::web::templates::{format_sats_as_btc, is_node_running, layout_with_status};
|
||||||
|
|
||||||
pub async fn balance_page(State(state): State<AppState>) -> Result<Html<String>, StatusCode> {
|
pub async fn balance_page(State(state): State<AppState>) -> Result<Html<String>, StatusCode> {
|
||||||
let balances = state.node.inner.list_balances();
|
let balances = state.node.inner.list_balances();
|
||||||
@@ -216,5 +216,8 @@ pub async fn balance_page(State(state): State<AppState>) -> Result<Html<String>,
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(Html(layout("Lightning", content).into_string()))
|
let is_running = is_node_running(&state.node.inner);
|
||||||
|
Ok(Html(
|
||||||
|
layout_with_status("Lightning", content, is_running).into_string(),
|
||||||
|
))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,8 @@ use serde::{Deserialize, Serialize};
|
|||||||
use crate::web::handlers::utils::deserialize_optional_u64;
|
use crate::web::handlers::utils::deserialize_optional_u64;
|
||||||
use crate::web::handlers::AppState;
|
use crate::web::handlers::AppState;
|
||||||
use crate::web::templates::{
|
use crate::web::templates::{
|
||||||
error_message, form_card, format_sats_as_btc, info_card, layout, success_message,
|
error_message, form_card, format_sats_as_btc, info_card, is_node_running, layout_with_status,
|
||||||
|
success_message,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Deserialize, Serialize)]
|
#[derive(Deserialize, Serialize)]
|
||||||
@@ -32,54 +33,8 @@ pub struct ConfirmOnchainForm {
|
|||||||
confirmed: Option<String>,
|
confirmed: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_new_address(State(state): State<AppState>) -> Result<Html<String>, StatusCode> {
|
fn quick_actions_section() -> maud::Markup {
|
||||||
let address_result = state.node.inner.onchain_payment().new_address();
|
html! {
|
||||||
|
|
||||||
let content = match address_result {
|
|
||||||
Ok(address) => {
|
|
||||||
html! {
|
|
||||||
(success_message(&format!("New address generated: {address}")))
|
|
||||||
div class="card" {
|
|
||||||
h2 { "Bitcoin Address" }
|
|
||||||
div class="info-item" {
|
|
||||||
span class="info-label" { "Address:" }
|
|
||||||
span class="info-value" style="font-family: monospace; font-size: 0.9rem;" { (address.to_string()) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
div class="card" {
|
|
||||||
a href="/onchain" { button { "← Back to On-chain" } }
|
|
||||||
" "
|
|
||||||
a href="/onchain/new-address" { button { "Generate Another Address" } }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
html! {
|
|
||||||
(error_message(&format!("Failed to generate address: {e}")))
|
|
||||||
div class="card" {
|
|
||||||
a href="/onchain" { button { "← Back to On-chain" } }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(Html(layout("New Address", content).into_string()))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn onchain_page(
|
|
||||||
State(state): State<AppState>,
|
|
||||||
query: Query<HashMap<String, String>>,
|
|
||||||
) -> Result<Html<String>, StatusCode> {
|
|
||||||
let balances = state.node.inner.list_balances();
|
|
||||||
let action = query
|
|
||||||
.get("action")
|
|
||||||
.map(|s| s.as_str())
|
|
||||||
.unwrap_or("overview");
|
|
||||||
|
|
||||||
let mut content = html! {
|
|
||||||
h2 style="text-align: center; margin-bottom: 3rem;" { "On-chain" }
|
|
||||||
|
|
||||||
// Quick Actions section - individual cards
|
|
||||||
div class="card" style="margin-bottom: 2rem;" {
|
div class="card" style="margin-bottom: 2rem;" {
|
||||||
h2 { "Quick Actions" }
|
h2 { "Quick Actions" }
|
||||||
div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 1.5rem; margin-top: 1.5rem;" {
|
div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 1.5rem; margin-top: 1.5rem;" {
|
||||||
@@ -102,6 +57,64 @@ pub async fn onchain_page(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_new_address(State(state): State<AppState>) -> Result<Html<String>, StatusCode> {
|
||||||
|
let address_result = state.node.inner.onchain_payment().new_address();
|
||||||
|
|
||||||
|
let content = match address_result {
|
||||||
|
Ok(address) => {
|
||||||
|
html! {
|
||||||
|
div class="card" {
|
||||||
|
h2 { "Bitcoin Address" }
|
||||||
|
div class="address-display" {
|
||||||
|
div class="address-container" {
|
||||||
|
span class="address-text" { (address.to_string()) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
div class="card" {
|
||||||
|
div style="display: flex; justify-content: space-between; gap: 1rem;" {
|
||||||
|
a href="/onchain" { button class="button-secondary" { "Back" } }
|
||||||
|
form method="post" action="/onchain/new-address" style="display: inline;" {
|
||||||
|
button class="button-primary" type="submit" { "Generate Another Address" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
html! {
|
||||||
|
(error_message(&format!("Failed to generate address: {e}")))
|
||||||
|
div class="card" {
|
||||||
|
a href="/onchain" { button class="button-primary" { "← Back to On-chain" } }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let is_running = is_node_running(&state.node.inner);
|
||||||
|
Ok(Html(
|
||||||
|
layout_with_status("New Address", content, is_running).into_string(),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn onchain_page(
|
||||||
|
State(state): State<AppState>,
|
||||||
|
query: Query<HashMap<String, String>>,
|
||||||
|
) -> Result<Html<String>, StatusCode> {
|
||||||
|
let balances = state.node.inner.list_balances();
|
||||||
|
let action = query
|
||||||
|
.get("action")
|
||||||
|
.map(|s| s.as_str())
|
||||||
|
.unwrap_or("overview");
|
||||||
|
|
||||||
|
let mut content = html! {
|
||||||
|
h2 style="text-align: center; margin-bottom: 3rem;" { "On-chain" }
|
||||||
|
|
||||||
|
// Quick Actions section - only show on overview
|
||||||
|
(quick_actions_section())
|
||||||
|
|
||||||
// On-chain Balance as metric cards
|
// On-chain Balance as metric cards
|
||||||
div class="card" {
|
div class="card" {
|
||||||
@@ -124,30 +137,6 @@ pub async fn onchain_page(
|
|||||||
content = html! {
|
content = html! {
|
||||||
h2 style="text-align: center; margin-bottom: 3rem;" { "On-chain" }
|
h2 style="text-align: center; margin-bottom: 3rem;" { "On-chain" }
|
||||||
|
|
||||||
// Quick Actions section - individual cards
|
|
||||||
div class="card" style="margin-bottom: 2rem;" {
|
|
||||||
h2 { "Quick Actions" }
|
|
||||||
div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 1.5rem; margin-top: 1.5rem;" {
|
|
||||||
// Receive Bitcoin Card
|
|
||||||
div class="quick-action-card" {
|
|
||||||
h3 style="font-size: 1.125rem; font-weight: 600; margin-bottom: 0.5rem; color: var(--text-primary);" { "Receive Bitcoin" }
|
|
||||||
p style="font-size: 0.875rem; color: var(--text-muted); margin-bottom: 1rem; line-height: 1.4;" { "Generate a new Bitcoin address to receive on-chain payments from other users or services." }
|
|
||||||
a href="/onchain?action=receive" style="text-decoration: none;" {
|
|
||||||
button class="button-outline" { "Receive Bitcoin" }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Send Bitcoin Card
|
|
||||||
div class="quick-action-card" {
|
|
||||||
h3 style="font-size: 1.125rem; font-weight: 600; margin-bottom: 0.5rem; color: var(--text-primary);" { "Send Bitcoin" }
|
|
||||||
p style="font-size: 0.875rem; color: var(--text-muted); margin-bottom: 1rem; line-height: 1.4;" { "Send Bitcoin to another address on the blockchain. Standard on-chain transactions." }
|
|
||||||
a href="/onchain?action=send" style="text-decoration: none;" {
|
|
||||||
button class="button-outline" { "Send Bitcoin" }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Send form above balance
|
// Send form above balance
|
||||||
(form_card(
|
(form_card(
|
||||||
"Send On-chain Payment",
|
"Send On-chain Payment",
|
||||||
@@ -193,30 +182,6 @@ pub async fn onchain_page(
|
|||||||
content = html! {
|
content = html! {
|
||||||
h2 style="text-align: center; margin-bottom: 3rem;" { "On-chain" }
|
h2 style="text-align: center; margin-bottom: 3rem;" { "On-chain" }
|
||||||
|
|
||||||
// Quick Actions section - individual cards
|
|
||||||
div class="card" style="margin-bottom: 2rem;" {
|
|
||||||
h2 { "Quick Actions" }
|
|
||||||
div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 1.5rem; margin-top: 1.5rem;" {
|
|
||||||
// Receive Bitcoin Card
|
|
||||||
div class="quick-action-card" {
|
|
||||||
h3 style="font-size: 1.125rem; font-weight: 600; margin-bottom: 0.5rem; color: var(--text-primary);" { "Receive Bitcoin" }
|
|
||||||
p style="font-size: 0.875rem; color: var(--text-muted); margin-bottom: 1rem; line-height: 1.4;" { "Generate a new Bitcoin address to receive on-chain payments from other users or services." }
|
|
||||||
a href="/onchain?action=receive" style="text-decoration: none;" {
|
|
||||||
button class="button-outline" { "Receive Bitcoin" }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Send Bitcoin Card
|
|
||||||
div class="quick-action-card" {
|
|
||||||
h3 style="font-size: 1.125rem; font-weight: 600; margin-bottom: 0.5rem; color: var(--text-primary);" { "Send Bitcoin" }
|
|
||||||
p style="font-size: 0.875rem; color: var(--text-muted); margin-bottom: 1rem; line-height: 1.4;" { "Send Bitcoin to another address on the blockchain. Standard on-chain transactions." }
|
|
||||||
a href="/onchain?action=send" style="text-decoration: none;" {
|
|
||||||
button class="button-outline" { "Send Bitcoin" }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generate address form above balance
|
// Generate address form above balance
|
||||||
(form_card(
|
(form_card(
|
||||||
"Generate New Address",
|
"Generate New Address",
|
||||||
@@ -252,7 +217,10 @@ pub async fn onchain_page(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(Html(layout("On-chain", content).into_string()))
|
let is_running = is_node_running(&state.node.inner);
|
||||||
|
Ok(Html(
|
||||||
|
layout_with_status("On-chain", content, is_running).into_string(),
|
||||||
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn post_send_onchain(
|
pub async fn post_send_onchain(
|
||||||
@@ -294,7 +262,7 @@ pub async fn onchain_confirm_page(
|
|||||||
.status(StatusCode::BAD_REQUEST)
|
.status(StatusCode::BAD_REQUEST)
|
||||||
.header("content-type", "text/html")
|
.header("content-type", "text/html")
|
||||||
.body(Body::from(
|
.body(Body::from(
|
||||||
layout("Send On-chain Error", content).into_string(),
|
layout_with_status("Send On-chain Error", content, true).into_string(),
|
||||||
))
|
))
|
||||||
.unwrap());
|
.unwrap());
|
||||||
}
|
}
|
||||||
@@ -320,7 +288,7 @@ pub async fn onchain_confirm_page(
|
|||||||
.status(StatusCode::BAD_REQUEST)
|
.status(StatusCode::BAD_REQUEST)
|
||||||
.header("content-type", "text/html")
|
.header("content-type", "text/html")
|
||||||
.body(Body::from(
|
.body(Body::from(
|
||||||
layout("Send On-chain Error", content).into_string(),
|
layout_with_status("Send On-chain Error", content, true).into_string(),
|
||||||
))
|
))
|
||||||
.unwrap());
|
.unwrap());
|
||||||
}
|
}
|
||||||
@@ -345,46 +313,46 @@ pub async fn onchain_confirm_page(
|
|||||||
let content = html! {
|
let content = html! {
|
||||||
h2 style="text-align: center; margin-bottom: 3rem;" { "Confirm On-chain Transaction" }
|
h2 style="text-align: center; margin-bottom: 3rem;" { "Confirm On-chain Transaction" }
|
||||||
|
|
||||||
div class="card" style="border: 2px solid hsl(var(--primary)); background-color: hsl(var(--primary) / 0.05);" {
|
|
||||||
h2 { "⚠️ Transaction Confirmation" }
|
|
||||||
p style="color: hsl(var(--muted-foreground)); margin-bottom: 1.5rem;" {
|
|
||||||
"Please review the transaction details carefully before proceeding. This action cannot be undone."
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
(info_card(
|
|
||||||
"Transaction Details",
|
|
||||||
vec![
|
|
||||||
("Recipient Address", form.address.clone()),
|
|
||||||
("Amount to Send", if is_send_all {
|
|
||||||
format!("{} (All available funds)", format_sats_as_btc(amount_to_send))
|
|
||||||
} else {
|
|
||||||
format_sats_as_btc(amount_to_send)
|
|
||||||
}),
|
|
||||||
("Current Spendable Balance", format_sats_as_btc(spendable_balance)),
|
|
||||||
]
|
|
||||||
))
|
|
||||||
|
|
||||||
@if is_send_all {
|
@if is_send_all {
|
||||||
div class="card" style="border: 1px solid hsl(32.6 75.4% 55.1%); background-color: hsl(32.6 75.4% 55.1% / 0.1);" {
|
div class="card send-all-notice" {
|
||||||
h3 style="color: hsl(32.6 75.4% 55.1%);" { "Send All Notice" }
|
h3 { "Send All Notice" }
|
||||||
p style="color: hsl(32.6 75.4% 55.1%);" {
|
p {
|
||||||
"This transaction will send all available funds to the recipient address. "
|
"This transaction will send all available funds to the recipient address. Network fees will be deducted from the total amount automatically."
|
||||||
"Network fees will be deducted from the total amount automatically."
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Transaction Details Card
|
||||||
div class="card" {
|
div class="card" {
|
||||||
div style="display: flex; justify-content: space-between; gap: 1rem; margin-top: 2rem;" {
|
h2 { "Transaction Details" }
|
||||||
|
div class="transaction-details" {
|
||||||
|
div class="detail-row" {
|
||||||
|
span class="detail-label" { "Recipient Address:" }
|
||||||
|
span class="detail-value" { (form.address.clone()) }
|
||||||
|
}
|
||||||
|
div class="detail-row" {
|
||||||
|
span class="detail-label" { "Amount to Send:" }
|
||||||
|
span class="detail-value-amount" {
|
||||||
|
(if is_send_all {
|
||||||
|
format!("{} (All available funds)", format_sats_as_btc(amount_to_send))
|
||||||
|
} else {
|
||||||
|
format_sats_as_btc(amount_to_send)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
div class="detail-row" {
|
||||||
|
span class="detail-label" { "Current Spendable Balance:" }
|
||||||
|
span class="detail-value-amount" { (format_sats_as_btc(spendable_balance)) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
div style="display: flex; justify-content: space-between; gap: 1rem; margin-top: 2rem; padding-top: 1.5rem; border-top: 1px solid hsl(var(--border));" {
|
||||||
a href="/onchain?action=send" {
|
a href="/onchain?action=send" {
|
||||||
button type="button" class="button-secondary" { "Cancel" }
|
button type="button" class="button-secondary" { "Cancel" }
|
||||||
}
|
}
|
||||||
div style="display: flex; gap: 0.5rem;" {
|
a href=(confirmation_url) {
|
||||||
a href=(confirmation_url) {
|
button class="button-primary" {
|
||||||
button class="button-primary" {
|
"Confirm"
|
||||||
"✓ Confirm & Send Transaction"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -394,7 +362,7 @@ pub async fn onchain_confirm_page(
|
|||||||
Ok(Response::builder()
|
Ok(Response::builder()
|
||||||
.header("content-type", "text/html")
|
.header("content-type", "text/html")
|
||||||
.body(Body::from(
|
.body(Body::from(
|
||||||
layout("Confirm Transaction", content).into_string(),
|
layout_with_status("Confirm Transaction", content, true).into_string(),
|
||||||
))
|
))
|
||||||
.unwrap())
|
.unwrap())
|
||||||
}
|
}
|
||||||
@@ -427,7 +395,7 @@ async fn execute_onchain_transaction(
|
|||||||
.status(StatusCode::BAD_REQUEST)
|
.status(StatusCode::BAD_REQUEST)
|
||||||
.header("content-type", "text/html")
|
.header("content-type", "text/html")
|
||||||
.body(Body::from(
|
.body(Body::from(
|
||||||
layout("Send On-chain Error", content).into_string(),
|
layout_with_status("Send On-chain Error", content, true).into_string(),
|
||||||
))
|
))
|
||||||
.unwrap());
|
.unwrap());
|
||||||
}
|
}
|
||||||
@@ -506,7 +474,7 @@ async fn execute_onchain_transaction(
|
|||||||
Ok(Response::builder()
|
Ok(Response::builder()
|
||||||
.header("content-type", "text/html")
|
.header("content-type", "text/html")
|
||||||
.body(Body::from(
|
.body(Body::from(
|
||||||
layout("Send On-chain Result", content).into_string(),
|
layout_with_status("Send On-chain Result", content, true).into_string(),
|
||||||
))
|
))
|
||||||
.unwrap())
|
.unwrap())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,8 +15,8 @@ use serde::Deserialize;
|
|||||||
use crate::web::handlers::utils::{deserialize_optional_u64, get_paginated_payments_streaming};
|
use crate::web::handlers::utils::{deserialize_optional_u64, get_paginated_payments_streaming};
|
||||||
use crate::web::handlers::AppState;
|
use crate::web::handlers::AppState;
|
||||||
use crate::web::templates::{
|
use crate::web::templates::{
|
||||||
error_message, form_card, format_msats_as_btc, format_sats_as_btc, info_card, layout,
|
error_message, form_card, format_msats_as_btc, format_sats_as_btc, info_card, is_node_running,
|
||||||
payment_list_item, success_message,
|
layout_with_status, payment_list_item, success_message,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
@@ -236,12 +236,13 @@ pub async fn payments_page(
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(Html(layout("Payment History", content).into_string()))
|
let is_running = is_node_running(&state.node.inner);
|
||||||
|
Ok(Html(
|
||||||
|
layout_with_status("Payment History", content, is_running).into_string(),
|
||||||
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn send_payments_page(
|
pub async fn send_payments_page(State(state): State<AppState>) -> Result<Html<String>, StatusCode> {
|
||||||
State(_state): State<AppState>,
|
|
||||||
) -> Result<Html<String>, StatusCode> {
|
|
||||||
let content = html! {
|
let content = html! {
|
||||||
h2 style="text-align: center; margin-bottom: 3rem;" { "Send Payment" }
|
h2 style="text-align: center; margin-bottom: 3rem;" { "Send Payment" }
|
||||||
div class="grid" {
|
div class="grid" {
|
||||||
@@ -280,13 +281,12 @@ pub async fn send_payments_page(
|
|||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
div class="card" {
|
|
||||||
h3 { "Payment History" }
|
|
||||||
a href="/payments" { button { "View All Payments" } }
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(Html(layout("Send Payments", content).into_string()))
|
let is_running = is_node_running(&state.node.inner);
|
||||||
|
Ok(Html(
|
||||||
|
layout_with_status("Send Payments", content, is_running).into_string(),
|
||||||
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn post_pay_bolt11(
|
pub async fn post_pay_bolt11(
|
||||||
@@ -306,7 +306,9 @@ pub async fn post_pay_bolt11(
|
|||||||
return Ok(Response::builder()
|
return Ok(Response::builder()
|
||||||
.status(StatusCode::BAD_REQUEST)
|
.status(StatusCode::BAD_REQUEST)
|
||||||
.header("content-type", "text/html")
|
.header("content-type", "text/html")
|
||||||
.body(Body::from(layout("Payment Error", content).into_string()))
|
.body(Body::from(
|
||||||
|
layout_with_status("Payment Error", content, true).into_string(),
|
||||||
|
))
|
||||||
.unwrap());
|
.unwrap());
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -348,7 +350,9 @@ pub async fn post_pay_bolt11(
|
|||||||
return Ok(Response::builder()
|
return Ok(Response::builder()
|
||||||
.status(StatusCode::INTERNAL_SERVER_ERROR)
|
.status(StatusCode::INTERNAL_SERVER_ERROR)
|
||||||
.header("content-type", "text/html")
|
.header("content-type", "text/html")
|
||||||
.body(Body::from(layout("Payment Error", content).into_string()))
|
.body(Body::from(
|
||||||
|
layout_with_status("Payment Error", content, true).into_string(),
|
||||||
|
))
|
||||||
.unwrap());
|
.unwrap());
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -433,7 +437,9 @@ pub async fn post_pay_bolt11(
|
|||||||
|
|
||||||
Ok(Response::builder()
|
Ok(Response::builder()
|
||||||
.header("content-type", "text/html")
|
.header("content-type", "text/html")
|
||||||
.body(Body::from(layout("Payment Result", content).into_string()))
|
.body(Body::from(
|
||||||
|
layout_with_status("Payment Result", content, true).into_string(),
|
||||||
|
))
|
||||||
.unwrap())
|
.unwrap())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -454,7 +460,9 @@ pub async fn post_pay_bolt12(
|
|||||||
return Ok(Response::builder()
|
return Ok(Response::builder()
|
||||||
.status(StatusCode::BAD_REQUEST)
|
.status(StatusCode::BAD_REQUEST)
|
||||||
.header("content-type", "text/html")
|
.header("content-type", "text/html")
|
||||||
.body(Body::from(layout("Payment Error", content).into_string()))
|
.body(Body::from(
|
||||||
|
layout_with_status("Payment Error", content, true).into_string(),
|
||||||
|
))
|
||||||
.unwrap());
|
.unwrap());
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -486,7 +494,9 @@ pub async fn post_pay_bolt12(
|
|||||||
return Ok(Response::builder()
|
return Ok(Response::builder()
|
||||||
.status(StatusCode::BAD_REQUEST)
|
.status(StatusCode::BAD_REQUEST)
|
||||||
.header("content-type", "text/html")
|
.header("content-type", "text/html")
|
||||||
.body(Body::from(layout("Payment Error", content).into_string()))
|
.body(Body::from(
|
||||||
|
layout_with_status("Payment Error", content, true).into_string(),
|
||||||
|
))
|
||||||
.unwrap());
|
.unwrap());
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -518,7 +528,9 @@ pub async fn post_pay_bolt12(
|
|||||||
return Ok(Response::builder()
|
return Ok(Response::builder()
|
||||||
.status(StatusCode::INTERNAL_SERVER_ERROR)
|
.status(StatusCode::INTERNAL_SERVER_ERROR)
|
||||||
.header("content-type", "text/html")
|
.header("content-type", "text/html")
|
||||||
.body(Body::from(layout("Payment Error", content).into_string()))
|
.body(Body::from(
|
||||||
|
layout_with_status("Payment Error", content, true).into_string(),
|
||||||
|
))
|
||||||
.unwrap());
|
.unwrap());
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -610,6 +622,8 @@ pub async fn post_pay_bolt12(
|
|||||||
|
|
||||||
Ok(Response::builder()
|
Ok(Response::builder()
|
||||||
.header("content-type", "text/html")
|
.header("content-type", "text/html")
|
||||||
.body(Body::from(layout("Payment Result", content).into_string()))
|
.body(Body::from(
|
||||||
|
layout_with_status("Payment Result", content, true).into_string(),
|
||||||
|
))
|
||||||
.unwrap())
|
.unwrap())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,12 @@
|
|||||||
|
use ldk_node::Node;
|
||||||
use maud::{html, Markup, DOCTYPE};
|
use maud::{html, Markup, DOCTYPE};
|
||||||
|
|
||||||
pub fn layout(title: &str, content: Markup) -> Markup {
|
/// Helper function to check if the node is running
|
||||||
|
pub fn is_node_running(node: &Node) -> bool {
|
||||||
|
node.status().is_running
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn layout_with_status(title: &str, content: Markup, is_running: bool) -> Markup {
|
||||||
html! {
|
html! {
|
||||||
(DOCTYPE)
|
(DOCTYPE)
|
||||||
html lang="en" {
|
html lang="en" {
|
||||||
@@ -252,12 +258,21 @@ pub fn layout(title: &str, content: Markup) -> Markup {
|
|||||||
box-shadow: 0 0 0 2px rgba(16, 185, 129, 0.2);
|
box-shadow: 0 0 0 2px rgba(16, 185, 129, 0.2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.status-indicator.status-inactive {
|
||||||
|
background-color: #ef4444;
|
||||||
|
box-shadow: 0 0 0 2px rgba(239, 68, 68, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
.status-text {
|
.status-text {
|
||||||
font-size: 0.875rem;
|
font-size: 0.875rem;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
color: #10b981;
|
color: #10b981;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.status-text.status-inactive {
|
||||||
|
color: #ef4444;
|
||||||
|
}
|
||||||
|
|
||||||
.node-title {
|
.node-title {
|
||||||
font-size: 1.875rem;
|
font-size: 1.875rem;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
@@ -281,38 +296,104 @@ pub fn layout(title: &str, content: Markup) -> Markup {
|
|||||||
|
|
||||||
/* Responsive header */
|
/* Responsive header */
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
|
header {
|
||||||
|
height: 180px; /* Slightly taller for better mobile layout */
|
||||||
|
padding: 1rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
header .container {
|
||||||
|
padding: 0 1rem;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
.header-content {
|
.header-content {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
width: 100%;
|
||||||
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-left {
|
.header-left {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-avatar {
|
||||||
|
width: 64px;
|
||||||
|
height: 64px;
|
||||||
|
padding: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-avatar-image {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.node-title {
|
.node-title {
|
||||||
font-size: 1.5rem;
|
font-size: 1.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.node-subtitle {
|
||||||
|
font-size: 0.8125rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.node-status {
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
header {
|
||||||
|
height: 160px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-avatar {
|
||||||
|
width: 56px;
|
||||||
|
height: 56px;
|
||||||
|
padding: 0.375rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-avatar-image {
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.node-title {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.node-subtitle {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dark mode navigation styles */
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
nav a {
|
nav a {
|
||||||
color: var(--text-muted) !important;
|
color: var(--text-muted) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
nav a:hover {
|
nav a:hover {
|
||||||
color: var(--text-secondary) !important;
|
color: var(--text-secondary) !important;
|
||||||
background-color: rgba(255, 255, 255, 0.05) !important;
|
background-color: rgba(255, 255, 255, 0.08) !important;
|
||||||
|
transform: translateY(-1px) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
nav a.active {
|
nav a.active {
|
||||||
color: var(--text-primary) !important;
|
color: var(--text-primary) !important;
|
||||||
background-color: rgba(255, 255, 255, 0.08) !important;
|
background-color: rgba(255, 255, 255, 0.1) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
nav a.active:hover {
|
nav a.active:hover {
|
||||||
background-color: rgba(255, 255, 255, 0.1) !important;
|
background-color: rgba(255, 255, 255, 0.12) !important;
|
||||||
|
transform: translateY(-1px) !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -409,23 +490,6 @@ pub fn layout(title: &str, content: Markup) -> Markup {
|
|||||||
line-height: 1.6;
|
line-height: 1.6;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
header {
|
|
||||||
height: 150px; /* Smaller height on mobile */
|
|
||||||
}
|
|
||||||
|
|
||||||
header .container {
|
|
||||||
padding: 0 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
h1 {
|
|
||||||
font-size: 2.25rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.subtitle {
|
|
||||||
font-size: 1.1rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Card fade-in animation */
|
/* Card fade-in animation */
|
||||||
@keyframes fade-in {
|
@keyframes fade-in {
|
||||||
@@ -491,6 +555,15 @@ pub fn layout(title: &str, content: Markup) -> Markup {
|
|||||||
background-color: hsl(var(--muted));
|
background-color: hsl(var(--muted));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Light mode navigation hover states */
|
||||||
|
@media (prefers-color-scheme: light) {
|
||||||
|
nav a:hover {
|
||||||
|
color: hsl(var(--foreground));
|
||||||
|
background-color: hsl(var(--muted) / 0.8);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
nav a.active {
|
nav a.active {
|
||||||
color: hsl(var(--primary-foreground));
|
color: hsl(var(--primary-foreground));
|
||||||
background-color: hsl(var(--primary));
|
background-color: hsl(var(--primary));
|
||||||
@@ -587,6 +660,20 @@ pub fn layout(title: &str, content: Markup) -> Markup {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Dark mode input field improvements */
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
input, textarea, select {
|
||||||
|
background-color: hsl(0 0% 8%);
|
||||||
|
border: 1px solid hsl(0 0% 20%);
|
||||||
|
color: hsl(var(--foreground));
|
||||||
|
}
|
||||||
|
|
||||||
|
input:focus, textarea:focus, select:focus {
|
||||||
|
background-color: hsl(0 0% 10%);
|
||||||
|
border-color: hsl(var(--ring));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
input:focus, textarea:focus, select:focus {
|
input:focus, textarea:focus, select:focus {
|
||||||
outline: 2px solid transparent;
|
outline: 2px solid transparent;
|
||||||
outline-offset: 2px;
|
outline-offset: 2px;
|
||||||
@@ -599,6 +686,74 @@ pub fn layout(title: &str, content: Markup) -> Markup {
|
|||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Subtle pagination dropdown styling */
|
||||||
|
.per-page-selector {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
margin: 1rem 0 0 0;
|
||||||
|
padding: 0;
|
||||||
|
background-color: transparent;
|
||||||
|
border: none;
|
||||||
|
border-radius: 0;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.per-page-selector label {
|
||||||
|
color: hsl(var(--muted-foreground));
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.per-page-selector select {
|
||||||
|
background-color: transparent;
|
||||||
|
border: 1px solid hsl(var(--muted));
|
||||||
|
border-radius: calc(var(--radius) - 2px);
|
||||||
|
padding: 0.25rem 0.5rem;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
color: hsl(var(--muted-foreground));
|
||||||
|
min-width: 50px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
flex: none;
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.per-page-selector select:hover {
|
||||||
|
border-color: hsl(var(--ring));
|
||||||
|
background-color: hsl(var(--muted) / 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.per-page-selector select:focus {
|
||||||
|
outline: 2px solid transparent;
|
||||||
|
outline-offset: 2px;
|
||||||
|
border-color: hsl(var(--ring));
|
||||||
|
box-shadow: 0 0 0 2px hsl(var(--ring) / 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.per-page-selector span {
|
||||||
|
color: hsl(var(--muted-foreground));
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Form actions layout */
|
||||||
|
.form-actions {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1rem;
|
||||||
|
margin-top: 1.5rem;
|
||||||
|
padding-top: 1rem;
|
||||||
|
border-top: 1px solid hsl(var(--border));
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-actions .button-secondary {
|
||||||
|
order: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-actions .button-primary {
|
||||||
|
order: 2;
|
||||||
|
}
|
||||||
|
|
||||||
button {
|
button {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -1082,6 +1237,14 @@ pub fn layout(title: &str, content: Markup) -> Markup {
|
|||||||
margin-bottom: 1.5rem;
|
margin-bottom: 1.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Dark mode payment card improvements - match other cards */
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
.payment-item {
|
||||||
|
background-color: rgba(255, 255, 255, 0.03) !important;
|
||||||
|
border: none !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.payment-header {
|
.payment-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
@@ -1266,6 +1429,39 @@ pub fn layout(title: &str, content: Markup) -> Markup {
|
|||||||
border: 1px solid hsl(32 95% 44% / 0.2);
|
border: 1px solid hsl(32 95% 44% / 0.2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Dark mode payment type badge improvements */
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
.payment-type-onchain {
|
||||||
|
background-color: hsl(32 95% 60% / 0.15);
|
||||||
|
color: hsl(32 95% 70%);
|
||||||
|
border: 1px solid hsl(32 95% 60% / 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.payment-type-bolt11 {
|
||||||
|
background-color: hsl(217 91% 70% / 0.15);
|
||||||
|
color: hsl(217 91% 80%);
|
||||||
|
border: 1px solid hsl(217 91% 70% / 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.payment-type-bolt12 {
|
||||||
|
background-color: hsl(262 83% 70% / 0.15);
|
||||||
|
color: hsl(262 83% 80%);
|
||||||
|
border: 1px solid hsl(262 83% 70% / 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.payment-type-spontaneous {
|
||||||
|
background-color: hsl(142.1 70.6% 60% / 0.15);
|
||||||
|
color: hsl(142.1 70.6% 75%);
|
||||||
|
border: 1px solid hsl(142.1 70.6% 60% / 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.payment-type-bolt11-jit {
|
||||||
|
background-color: hsl(199 89% 65% / 0.15);
|
||||||
|
color: hsl(199 89% 80%);
|
||||||
|
border: 1px solid hsl(199 89% 65% / 0.3);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.payment-type-spontaneous {
|
.payment-type-spontaneous {
|
||||||
background-color: hsl(142.1 70.6% 45.3% / 0.1);
|
background-color: hsl(142.1 70.6% 45.3% / 0.1);
|
||||||
color: hsl(142.1 70.6% 45.3%);
|
color: hsl(142.1 70.6% 45.3%);
|
||||||
@@ -1635,6 +1831,147 @@ pub fn layout(title: &str, content: Markup) -> Markup {
|
|||||||
fill: rgba(156, 163, 175, 0.2);
|
fill: rgba(156, 163, 175, 0.2);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Address display styling */
|
||||||
|
.address-display {
|
||||||
|
margin: 1.5rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.address-container {
|
||||||
|
padding: 1rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.address-text {
|
||||||
|
font-family: 'SF Mono', 'Monaco', 'Inconsolata', 'Roboto Mono', 'Courier New', monospace;
|
||||||
|
font-size: 1.25rem;
|
||||||
|
font-weight: 500;
|
||||||
|
color: hsl(var(--foreground));
|
||||||
|
word-break: break-all;
|
||||||
|
overflow-wrap: break-word;
|
||||||
|
hyphens: auto;
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
line-height: 1.4;
|
||||||
|
background-color: transparent;
|
||||||
|
border: none;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* Dark mode address styling */
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
.address-text {
|
||||||
|
color: var(--text-primary) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive address display */
|
||||||
|
@media (max-width: 640px) {
|
||||||
|
.address-text {
|
||||||
|
font-size: 1.125rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Transaction confirmation styling */
|
||||||
|
.transaction-details {
|
||||||
|
margin-top: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.transaction-details .detail-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
gap: 1rem;
|
||||||
|
padding: 0.75rem 0;
|
||||||
|
border-bottom: 1px solid hsl(var(--border));
|
||||||
|
}
|
||||||
|
|
||||||
|
.transaction-details .detail-row:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.transaction-details .detail-label {
|
||||||
|
font-weight: 500;
|
||||||
|
color: hsl(var(--muted-foreground));
|
||||||
|
font-size: 0.875rem;
|
||||||
|
min-width: 180px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.transaction-details .detail-value {
|
||||||
|
color: hsl(var(--foreground));
|
||||||
|
font-family: 'SF Mono', 'Monaco', 'Inconsolata', 'Roboto Mono', 'Courier New', monospace;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
word-break: break-all;
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.transaction-details .detail-value-amount {
|
||||||
|
color: hsl(var(--foreground));
|
||||||
|
font-family: 'SF Mono', 'Monaco', 'Inconsolata', 'Roboto Mono', 'Courier New', monospace;
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.send-all-notice {
|
||||||
|
border: 1px solid hsl(32.6 75.4% 55.1%);
|
||||||
|
background-color: hsl(32.6 75.4% 55.1% / 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.send-all-notice h3 {
|
||||||
|
color: hsl(32.6 75.4% 55.1%);
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.send-all-notice p {
|
||||||
|
color: hsl(32.6 75.4% 55.1%);
|
||||||
|
font-size: 0.875rem;
|
||||||
|
line-height: 1.4;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dark mode transaction styling */
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
.transaction-details .detail-label {
|
||||||
|
color: var(--text-muted) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.transaction-details .detail-value,
|
||||||
|
.transaction-details .detail-value-amount {
|
||||||
|
color: var(--text-primary) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.send-all-notice {
|
||||||
|
background-color: hsl(32.6 75.4% 55.1% / 0.15) !important;
|
||||||
|
border-color: hsl(32.6 75.4% 55.1% / 0.3) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive transaction details */
|
||||||
|
@media (max-width: 640px) {
|
||||||
|
.transaction-details .detail-row {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.transaction-details .detail-label {
|
||||||
|
min-width: auto;
|
||||||
|
font-size: 0.8125rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.transaction-details .detail-value,
|
||||||
|
.transaction-details .detail-value-amount {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
"
|
"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1648,8 +1985,13 @@ pub fn layout(title: &str, content: Markup) -> Markup {
|
|||||||
}
|
}
|
||||||
div class="node-info" {
|
div class="node-info" {
|
||||||
div class="node-status" {
|
div class="node-status" {
|
||||||
span class="status-indicator status-running" {}
|
@if is_running {
|
||||||
span class="status-text" { "Running" }
|
span class="status-indicator" {}
|
||||||
|
span class="status-text" { "Running" }
|
||||||
|
} @else {
|
||||||
|
span class="status-indicator status-inactive" {}
|
||||||
|
span class="status-text status-inactive" { "Inactive" }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
h1 class="node-title" { "CDK LDK Node" }
|
h1 class="node-title" { "CDK LDK Node" }
|
||||||
span class="node-subtitle" { "Cashu Mint & Lightning Network Node Management" }
|
span class="node-subtitle" { "Cashu Mint & Lightning Network Node Management" }
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 43 KiB After Width: | Height: | Size: 353 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 606 KiB After Width: | Height: | Size: 144 KiB |
Reference in New Issue
Block a user